From 8cf5720db9d0e60c29fb6600ce5eb17adae9966d Mon Sep 17 00:00:00 2001 From: Carl-Robert Date: Tue, 2 Apr 2024 02:50:41 +0300 Subject: [PATCH 01/20] feat: OpenAI and Claude vision support (#430) * feat: add OpenAI and Claude vision support * refactor: replace awaitility with PlatformTestUtil.waitWithEventsDispatching * feat: display error when image not found * chore: bump llm-client * feat: configurable file watcher and minor code cleanup * fix: ensure image notifications are triggered only for image file types * docs: update changelog * fix: user textarea icon button behaviour * refactor: minor cleanup --- CHANGELOG.md | 4 + build.gradle.kts | 2 - .../codegpt.java-conventions.gradle.kts | 2 +- .../ee/carlrobert/codegpt/CodeGPTKeys.java | 2 + .../carlrobert/codegpt/EncodingManager.java | 14 +- .../java/ee/carlrobert/codegpt/Icons.java | 1 + .../ProjectCompilationStatusListener.java | 2 +- .../codegpt/completions/CallParameters.java | 19 ++ .../CompletionRequestProvider.java | 92 +++++++-- .../completions/CompletionRequestService.java | 11 +- .../conversations/message/Message.java | 10 + .../configuration/ConfigurationComponent.java | 7 + .../configuration/ConfigurationState.java | 9 + .../chat/ChatToolWindowTabPanel.java | 72 ++++--- .../standard/StandardChatToolWindowPanel.java | 76 +++++-- .../StandardChatToolWindowTabPanel.java | 10 +- .../toolwindow/chat/ui/ImageAccordion.java | 86 ++++++++ .../chat/ui/SelectedFilesNotification.java | 86 -------- .../chat/ui/ToolWindowFooterNotification.java | 50 +++++ .../toolwindow/chat/ui/UserMessagePanel.java | 16 ++ .../chat/ui/textarea/AttachImageNotifier.java | 11 + .../chat/ui/textarea/ModelComboBoxAction.java | 27 +-- .../chat/ui/textarea/UserPromptTextArea.java | 65 +++--- .../ui/textarea/UserPromptTextAreaHeader.java | 31 +-- .../codegpt/util/file/FileUtil.java | 9 + .../codegpt/CodeGPTProjectActivity.kt | 46 ++++- .../codegpt/CodeGPTUpdateActivity.kt | 6 +- .../ee/carlrobert/codegpt/FileWatcher.kt | 29 +++ .../codegpt/actions/AttachImageAction.kt | 40 ++++ src/main/resources/icons/send.svg | 2 +- src/main/resources/icons/send_dark.svg | 2 +- src/main/resources/icons/upload.svg | 7 + src/main/resources/icons/upload_dark.svg | 7 + .../resources/messages/codegpt.properties | 11 +- .../CodeCompletionServiceTest.java | 5 +- .../DefaultCompletionRequestHandlerTest.java | 10 +- .../StandardChatToolWindowTabPanelTest.java | 194 +++++++++++++----- .../java/testsupport/IntegrationTest.java | 14 +- .../testsupport/mixin/ShortcutsTestMixin.java | 15 +- src/test/resources/images/test-image.png | Bin 0 -> 3940 bytes 40 files changed, 793 insertions(+), 309 deletions(-) create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt create mode 100644 src/main/resources/icons/upload.svg create mode 100644 src/main/resources/icons/upload_dark.svg create mode 100644 src/test/resources/images/test-image.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae98a6d..dab7732f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models + ### Removed - Azure custom configuration (use OpenAI-compatible service to override the default configuration) diff --git a/build.gradle.kts b/build.gradle.kts index d942d5a6..f6c9f1d1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,8 +59,6 @@ dependencies { implementation("org.jsoup:jsoup:1.17.2") implementation("org.apache.commons:commons-text:1.11.0") implementation("com.knuddels:jtokkit:1.0.0") - - testImplementation("org.awaitility:awaitility:4.2.0") } tasks.register("updateSubmodules") { diff --git a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts index 4a2878aa..767f856a 100644 --- a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts @@ -26,7 +26,7 @@ checkstyle { } dependencies { - implementation("ee.carlrobert:llm-client:0.6.2") + implementation("ee.carlrobert:llm-client:0.7.0") testImplementation("org.assertj:assertj-core:3.25.3") testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java index 3cf177f2..a96ade5b 100644 --- a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java @@ -9,4 +9,6 @@ public class CodeGPTKeys { Key.create("codegpt.editor.inlay.prev-value"); public static final Key> SELECTED_FILES = Key.create("codegpt.selectedFiles"); + public static final Key IMAGE_ATTACHMENT_FILE_PATH = + Key.create("codegpt.imageAttachmentFilePath"); } diff --git a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java index 8e3962d5..46022bfa 100644 --- a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java +++ b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java @@ -9,7 +9,10 @@ import com.knuddels.jtokkit.api.EncodingRegistry; import com.knuddels.jtokkit.api.EncodingType; import com.knuddels.jtokkit.api.IntArrayList; import ee.carlrobert.codegpt.conversations.Conversation; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent; import java.util.List; @Service @@ -38,7 +41,16 @@ public final class EncodingManager { } public int countMessageTokens(OpenAIChatCompletionMessage message) { - return countMessageTokens(message.getRole(), message.getContent()); + if (message instanceof OpenAIChatCompletionStandardMessage standardMessage) { + return countMessageTokens(standardMessage.getRole(), standardMessage.getContent()); + } + + return ((OpenAIChatCompletionDetailedMessage) message).getContent().stream() + .filter(it -> it instanceof OpenAIMessageTextContent) + .map(it -> countMessageTokens( + ((OpenAIChatCompletionDetailedMessage) message).getRole(), + ((OpenAIMessageTextContent) it).getText())) + .reduce(0, Integer::sum); } public int countMessageTokens(String role, String content) { diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index d7aae08b..6dbeaf12 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -17,4 +17,5 @@ public final class Icons { public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class); public static final Icon YouSmall = IconLoader.getIcon("/icons/you_small.png", Icons.class); public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class); + public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class); } diff --git a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java index 73d292c2..b1b86157 100644 --- a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java +++ b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java @@ -53,7 +53,7 @@ public class ProjectCompilationStatusListener implements CompilationStatusListen () -> project.getService(StandardChatToolWindowContentManager.class) .sendMessage(getMultiFileMessage(compileContext), FIX_COMPILE_ERRORS))) .addAction(NotificationAction.createSimpleExpiring( - CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton"), + CodeGPTBundle.get("shared.notification.doNotShowAgain"), () -> ConfigurationSettings.getCurrentState().setCaptureCompileErrors(false))) .notify(project); } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java index 3123da64..68d8134d 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.completions; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.message.Message; +import org.jetbrains.annotations.Nullable; public class CallParameters { @@ -9,6 +10,8 @@ public class CallParameters { private final ConversationType conversationType; private final Message message; private final boolean retry; + private @Nullable String imageMediaType; + private byte[] imageData; public CallParameters(Conversation conversation, Message message) { this(conversation, ConversationType.DEFAULT, message, false); @@ -40,4 +43,20 @@ public class CallParameters { public boolean isRetry() { return retry; } + + public String getImageMediaType() { + return imageMediaType; + } + + public void setImageMediaType(String imageMediaType) { + this.imageMediaType = imageMediaType; + } + + public byte[] getImageData() { + return imageData; + } + + public void setImageData(byte[] imageData) { + this.imageData = imageData; + } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index d43109fb..328cd04b 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -30,15 +30,29 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration; import ee.carlrobert.codegpt.telemetry.core.service.UserId; +import ee.carlrobert.codegpt.util.file.FileUtil; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeBase64Source; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionDetailedMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionMessage; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; -import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageImageContent; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageTextContent; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIImageUrl; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageImageURLContent; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent; import ee.carlrobert.llm.client.you.completion.YouCompletionRequest; import ee.carlrobert.llm.client.you.completion.YouCompletionRequestMessage; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -91,9 +105,10 @@ public class CompletionRequestProvider { public static OpenAIChatCompletionRequest buildOpenAILookupCompletionRequest(String context) { return new OpenAIChatCompletionRequest.Builder( List.of( - new OpenAIChatCompletionMessage("system", + new OpenAIChatCompletionStandardMessage( + "system", getResourceContent("/prompts/method-name-generator.txt")), - new OpenAIChatCompletionMessage("user", context))) + new OpenAIChatCompletionStandardMessage("user", context))) .setModel(OpenAISettings.getCurrentState().getModel()) .setStream(false) .build(); @@ -103,8 +118,8 @@ public class CompletionRequestProvider { return buildCustomOpenAIChatCompletionRequest( CustomServiceSettings.getCurrentState(), List.of( - new OpenAIChatCompletionMessage("system", system), - new OpenAIChatCompletionMessage("user", context)), + new OpenAIChatCompletionStandardMessage("system", system), + new OpenAIChatCompletionStandardMessage("user", context)), true); } @@ -112,10 +127,10 @@ public class CompletionRequestProvider { return buildCustomOpenAIChatCompletionRequest( CustomServiceSettings.getCurrentState(), List.of( - new OpenAIChatCompletionMessage( + new OpenAIChatCompletionStandardMessage( "system", getResourceContent("/prompts/method-name-generator.txt")), - new OpenAIChatCompletionMessage("user", context)), + new OpenAIChatCompletionStandardMessage("user", context)), false); } @@ -246,15 +261,25 @@ public class CompletionRequestProvider { request.setMaxTokens(configuration.getMaxTokens()); request.setStream(true); request.setSystem(COMPLETION_SYSTEM_PROMPT); - var messages = conversation.getMessages().stream() + List messages = conversation.getMessages().stream() .filter(prevMessage -> prevMessage.getResponse() != null && !prevMessage.getResponse().isEmpty()) .flatMap(prevMessage -> Stream.of( - new ClaudeCompletionRequestMessage("user", prevMessage.getPrompt()), - new ClaudeCompletionRequestMessage("assistant", prevMessage.getResponse()))) + new ClaudeCompletionStandardMessage("user", prevMessage.getPrompt()), + new ClaudeCompletionStandardMessage("assistant", prevMessage.getResponse()))) .collect(toList()); - messages.add( - new ClaudeCompletionRequestMessage("user", callParameters.getMessage().getPrompt())); + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + messages.add(new ClaudeCompletionDetailedMessage("user", + List.of( + new ClaudeMessageImageContent(new ClaudeBase64Source( + callParameters.getImageMediaType(), + callParameters.getImageData())), + new ClaudeMessageTextContent(callParameters.getMessage().getPrompt())))); + } else { + messages.add( + new ClaudeCompletionStandardMessage("user", callParameters.getMessage().getPrompt())); + } request.setMessages(messages); return request; } @@ -263,22 +288,48 @@ public class CompletionRequestProvider { var message = callParameters.getMessage(); var messages = new ArrayList(); if (callParameters.getConversationType() == ConversationType.DEFAULT) { - messages.add(new OpenAIChatCompletionMessage( + messages.add(new OpenAIChatCompletionStandardMessage( "system", ConfigurationSettings.getCurrentState().getSystemPrompt())); } if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { - messages.add(new OpenAIChatCompletionMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); + messages.add( + new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); } for (var prevMessage : conversation.getMessages()) { if (callParameters.isRetry() && prevMessage.getId().equals(message.getId())) { break; } - messages.add(new OpenAIChatCompletionMessage("user", prevMessage.getPrompt())); - messages.add(new OpenAIChatCompletionMessage("assistant", prevMessage.getResponse())); + var prevMessageImageFilePath = prevMessage.getImageFilePath(); + if (prevMessageImageFilePath != null && !prevMessageImageFilePath.isEmpty()) { + try { + var imageFilePath = Path.of(prevMessageImageFilePath); + var imageData = Files.readAllBytes(imageFilePath); + var imageMediaType = FileUtil.getImageMediaType(imageFilePath.getFileName().toString()); + messages.add(new OpenAIChatCompletionDetailedMessage("user", + List.of( + new OpenAIMessageImageURLContent(new OpenAIImageUrl(imageMediaType, imageData)), + new OpenAIMessageTextContent(prevMessage.getPrompt())))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + messages.add(new OpenAIChatCompletionStandardMessage("user", prevMessage.getPrompt())); + } + messages.add(new OpenAIChatCompletionStandardMessage("assistant", prevMessage.getResponse())); + } + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + messages.add(new OpenAIChatCompletionDetailedMessage("user", + List.of( + new OpenAIMessageImageURLContent( + new OpenAIImageUrl(callParameters.getImageMediaType(), + callParameters.getImageData())), + new OpenAIMessageTextContent(message.getPrompt())))); + } else { + messages.add(new OpenAIChatCompletionStandardMessage("user", message.getPrompt())); } - messages.add(new OpenAIChatCompletionMessage("user", message.getPrompt())); return messages; } @@ -324,8 +375,11 @@ public class CompletionRequestProvider { break; } - totalUsage -= encodingManager.countMessageTokens(messages.get(i)); - messages.set(i, null); + var message = messages.get(i); + if (message instanceof OpenAIChatCompletionStandardMessage) { + totalUsage -= encodingManager.countMessageTokens(message); + messages.set(i, null); + } } return messages.stream().filter(Objects::nonNull).collect(toList()); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 5805921b..ba4608ee 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -26,11 +26,11 @@ import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.DeserializationUtil; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; -import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener; -import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; import ee.carlrobert.llm.completion.CompletionEventListener; import java.io.IOException; @@ -117,8 +117,8 @@ public final class CompletionRequestService { var configuration = ConfigurationSettings.getCurrentState(); var commitMessagePrompt = configuration.getCommitMessagePrompt(); var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of( - new OpenAIChatCompletionMessage("system", commitMessagePrompt), - new OpenAIChatCompletionMessage("user", prompt))) + new OpenAIChatCompletionStandardMessage("system", commitMessagePrompt), + new OpenAIChatCompletionStandardMessage("user", prompt))) .setModel(OpenAISettings.getCurrentState().getModel()) .build(); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); @@ -142,8 +142,7 @@ public final class CompletionRequestService { claudeRequest.setStream(true); claudeRequest.setMaxTokens(configuration.getMaxTokens()); claudeRequest.setModel(anthropicSettings.getModel()); - claudeRequest.setMessages( - List.of(new ClaudeCompletionRequestMessage("user", prompt))); + claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", prompt))); CompletionClientProvider.getClaudeClient() .getCompletionAsync(claudeRequest, eventListener); break; diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java index 5ff03f71..9b1c05b5 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java @@ -6,6 +6,7 @@ import ee.carlrobert.llm.client.you.completion.YouSerpResult; import java.util.List; import java.util.Objects; import java.util.UUID; +import org.jetbrains.annotations.Nullable; public class Message { @@ -15,6 +16,7 @@ public class Message { private String userMessage; private List serpResults; private List referencedFilePaths; + private @Nullable String imageFilePath; public Message(String prompt, String response) { this(prompt); @@ -71,6 +73,14 @@ public class Message { this.referencedFilePaths = referencedFilePaths; } + public @Nullable String getImageFilePath() { + return imageFilePath; + } + + public void setImageFilePath(@Nullable String imageFilePath) { + this.imageFilePath = imageFilePath; + } + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index 54aa2713..cbcf7fa9 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -44,6 +44,7 @@ public class ConfigurationComponent { private final JPanel mainPanel; private final JBTable table; private final JBCheckBox checkForPluginUpdatesCheckBox; + private final JBCheckBox checkForNewScreenshotsCheckBox; private final JBCheckBox openNewTabCheckBox; private final JBCheckBox methodNameGenerationCheckBox; private final JBCheckBox autoFormattingCheckBox; @@ -111,6 +112,9 @@ public class ConfigurationComponent { checkForPluginUpdatesCheckBox = new JBCheckBox( CodeGPTBundle.get("configurationConfigurable.checkForPluginUpdates.label"), + configuration.isCheckForNewScreenshots()); + checkForNewScreenshotsCheckBox = new JBCheckBox( + CodeGPTBundle.get("configurationConfigurable.checkForNewScreenshots.label"), configuration.isCheckForPluginUpdates()); openNewTabCheckBox = new JBCheckBox( CodeGPTBundle.get("configurationConfigurable.openNewTabCheckBox.label"), @@ -126,6 +130,7 @@ public class ConfigurationComponent { .addComponent(tablePanel) .addVerticalGap(4) .addComponent(checkForPluginUpdatesCheckBox) + .addComponent(checkForNewScreenshotsCheckBox) .addComponent(openNewTabCheckBox) .addComponent(methodNameGenerationCheckBox) .addComponent(autoFormattingCheckBox) @@ -152,6 +157,7 @@ public class ConfigurationComponent { state.setSystemPrompt(systemPromptTextArea.getText()); state.setCommitMessagePrompt(commitMessagePromptTextArea.getText()); state.setCheckForPluginUpdates(checkForPluginUpdatesCheckBox.isSelected()); + state.setCheckForNewScreenshots(checkForNewScreenshotsCheckBox.isSelected()); state.setCreateNewChatOnEachAction(openNewTabCheckBox.isSelected()); state.setMethodNameGenerationEnabled(methodNameGenerationCheckBox.isSelected()); state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected()); @@ -168,6 +174,7 @@ public class ConfigurationComponent { systemPromptTextArea.setText(configuration.getSystemPrompt()); commitMessagePromptTextArea.setText(configuration.getCommitMessagePrompt()); checkForPluginUpdatesCheckBox.setSelected(configuration.isCheckForPluginUpdates()); + checkForNewScreenshotsCheckBox.setSelected(configuration.isCheckForNewScreenshots()); openNewTabCheckBox.setSelected(configuration.isCreateNewChatOnEachAction()); methodNameGenerationCheckBox.setSelected(configuration.isMethodNameGenerationEnabled()); autoFormattingCheckBox.setSelected(configuration.isAutoFormattingEnabled()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index 7ae70912..f11663d3 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -14,6 +14,7 @@ public class ConfigurationState { private int maxTokens = 1000; private double temperature = 0.1; private boolean checkForPluginUpdates = true; + private boolean checkForNewScreenshots = true; private boolean createNewChatOnEachAction; private boolean ignoreGitCommitTokenLimit; private boolean methodNameGenerationEnabled = true; @@ -62,6 +63,14 @@ public class ConfigurationState { this.createNewChatOnEachAction = createNewChatOnEachAction; } + public boolean isCheckForNewScreenshots() { + return checkForNewScreenshots; + } + + public void setCheckForNewScreenshots(boolean checkForNewScreenshots) { + this.checkForNewScreenshots = checkForNewScreenshots; + } + public Map getTableData() { return tableData; } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index b59867ed..a3a13784 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -6,7 +6,6 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; @@ -27,7 +26,6 @@ import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; @@ -41,11 +39,15 @@ import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.UUID; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public abstract class ChatToolWindowTabPanel implements Disposable { @@ -62,10 +64,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { protected abstract JComponent getLandingView(); - public ChatToolWindowTabPanel( - @NotNull Project project, - @NotNull Conversation conversation, - boolean useContextualSearch) { + public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) { this.project = project; this.conversation = conversation; conversationService = ConversationService.getInstance(); @@ -98,8 +97,10 @@ public abstract class ChatToolWindowTabPanel implements Disposable { } public void sendMessage(Message message, ConversationType conversationType) { - Runnable runnable = () -> { + SwingUtilities.invokeLater(() -> { var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); + var chatToolWindowPanel = project.getService(StandardChatToolWindowContentManager.class) + .tryFindChatToolWindowPanel(); if (referencedFiles != null && !referencedFiles.isEmpty()) { var referencedFilePaths = referencedFiles.stream() .map(ReferencedFile::getFilePath) @@ -110,26 +111,42 @@ public abstract class ChatToolWindowTabPanel implements Disposable { totalTokensPanel.updateReferencedFilesTokens(referencedFiles); - project.getService(StandardChatToolWindowContentManager.class) - .tryFindChatToolWindowPanel() - .ifPresent(StandardChatToolWindowPanel::clearSelectedFilesNotification); + chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project)); + } + + var userMessagePanel = new UserMessagePanel(project, message, this); + var attachedFilePath = CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project); + var callParameters = getCallParameters(conversationType, message, attachedFilePath); + if (callParameters.getImageData() != null) { + message.setImageFilePath(attachedFilePath); + chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project)); + userMessagePanel.displayImage(attachedFilePath); } var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(new UserMessagePanel(project, message, this)); + messagePanel.add(userMessagePanel); + var responsePanel = createResponsePanel(message, conversationType); messagePanel.add(responsePanel); - updateTotalTokens(message); + call(callParameters, responsePanel); + }); + } - call(message, conversationType, responsePanel, false); - }; - // TODO - if (ApplicationManager.getApplication().isUnitTestMode()) { - runnable.run(); - } else { - SwingUtilities.invokeLater(runnable); + private CallParameters getCallParameters( + ConversationType conversationType, + Message message, + @Nullable String attachedFilePath) { + var callParameters = new CallParameters(conversation, conversationType, message, false); + if (attachedFilePath != null && !attachedFilePath.isEmpty()) { + try { + callParameters.setImageData(Files.readAllBytes(Path.of(attachedFilePath))); + callParameters.setImageMediaType(FileUtil.getImageMediaType(attachedFilePath)); + } catch (IOException e) { + throw new RuntimeException(e); + } } + return callParameters; } private void updateTotalTokens(Message message) { @@ -175,7 +192,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { if (responsePanel != null) { message.setResponse(""); conversationService.saveMessage(conversation, message); - call(message, conversationType, responsePanel, true); + call(new CallParameters(conversation, conversationType, message, true), responsePanel); } totalTokensPanel.updateConversationTokens(conversation); @@ -202,11 +219,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { totalTokensPanel.updateConversationTokens(conversation); } - private void call( - Message message, - ConversationType conversationType, - ResponsePanel responsePanel, - boolean retry) { + private void call(CallParameters callParameters, ResponsePanel responsePanel) { var responseContainer = (ChatMessageResponseBody) responsePanel.getContent(); if (!CompletionRequestService.getInstance().isRequestAllowed()) { @@ -222,13 +235,13 @@ public abstract class ChatToolWindowTabPanel implements Disposable { userPromptTextArea) { @Override public void handleTokensExceededPolicyAccepted() { - call(message, conversationType, responsePanel, true); + call(callParameters, responsePanel); } }); userPromptTextArea.setRequestHandler(requestHandler); userPromptTextArea.setSubmitEnabled(false); - requestHandler.call(new CallParameters(conversation, conversationType, message, retry)); + requestHandler.call(callParameters); } private void handleSubmit(String text) { @@ -257,7 +270,10 @@ public abstract class ChatToolWindowTabPanel implements Disposable { panel.add(JBUI.Panels.simplePanel(new UserPromptTextAreaHeader( selectedService, totalTokensPanel, - contentManager::createNewTabPanel)), BorderLayout.NORTH); + () -> { + ConversationService.getInstance().startConversation(); + contentManager.createNewTabPanel(); + })), BorderLayout.NORTH); panel.add(JBUI.Panels.simplePanel(userPromptTextArea), BorderLayout.CENTER); return panel; } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java index 18e1d43b..178c024e 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java @@ -1,5 +1,8 @@ package ee.carlrobert.codegpt.toolwindow.chat.standard; +import static java.lang.String.format; +import static java.util.Collections.emptyList; + import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; @@ -8,6 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.SimpleToolWindowPanel; import com.intellij.openapi.util.Disposer; import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction; @@ -15,42 +19,78 @@ import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction; import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.ConversationsState; -import ee.carlrobert.codegpt.toolwindow.chat.ui.SelectedFilesNotification; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ToolWindowFooterNotification; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier; import java.awt.BorderLayout; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.stream.Collectors; +import javax.swing.BoxLayout; import javax.swing.JPanel; import org.jetbrains.annotations.NotNull; public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { - private final SelectedFilesNotification selectedFilesNotification; + private final ToolWindowFooterNotification selectedFilesNotification; + private final ToolWindowFooterNotification imageFileAttachmentNotification; private StandardChatToolWindowTabbedPane tabbedPane; public StandardChatToolWindowPanel( @NotNull Project project, @NotNull Disposable parentDisposable) { super(true); - selectedFilesNotification = new SelectedFilesNotification(project); - init(project, selectedFilesNotification, parentDisposable); + selectedFilesNotification = new ToolWindowFooterNotification( + () -> clearSelectedFilesNotification(project)); + imageFileAttachmentNotification = new ToolWindowFooterNotification(() -> + project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "")); + init(project, parentDisposable); project.getMessageBus() .connect() .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, (IncludeFilesInContextNotifier) this::displaySelectedFilesNotification); + project.getMessageBus() + .connect() + .subscribe(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC, + (AttachImageNotifier) filePath -> imageFileAttachmentNotification.show( + Path.of(filePath).getFileName().toString(), + "File path: " + filePath)); + } + + public StandardChatToolWindowTabbedPane getChatTabbedPane() { + return tabbedPane; } public void displaySelectedFilesNotification(List referencedFiles) { - selectedFilesNotification.displaySelectedFilesNotification(referencedFiles); + if (referencedFiles.isEmpty()) { + return; + } + + var referencedFilePaths = referencedFiles.stream() + .map(ReferencedFile::getFilePath) + .collect(Collectors.toList()); + selectedFilesNotification.show( + referencedFiles.size() + " files selected", + selectedFilesNotificationDescription(referencedFilePaths)); } - public void clearSelectedFilesNotification() { - selectedFilesNotification.clearSelectedFilesNotification(); + private String selectedFilesNotificationDescription(List referencedFilePaths) { + var html = referencedFilePaths.stream() + .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) + .collect(Collectors.joining()); + return format("
      %s
    ", html); } - private void init( - Project project, - SelectedFilesNotification selectedFilesNotification, - Disposable parentDisposable) { + public void clearNotifications(Project project) { + selectedFilesNotification.hideNotification(); + imageFileAttachmentNotification.hideNotification(); + + project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""); + project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); + } + + private void init(Project project, Disposable parentDisposable) { var conversation = ConversationsState.getCurrentConversation(); if (conversation == null) { conversation = ConversationService.getInstance().startConversation(); @@ -71,8 +111,11 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { BorderLayout.LINE_START); setToolbar(actionToolbarPanel); - setContent( - JBUI.Panels.simplePanel(tabbedPane).addToBottom(selectedFilesNotification)); + var notificationContainer = new JPanel(new BorderLayout()); + notificationContainer.setLayout(new BoxLayout(notificationContainer, BoxLayout.PAGE_AXIS)); + notificationContainer.add(selectedFilesNotification); + notificationContainer.add(imageFileAttachmentNotification); + setContent(JBUI.Panels.simplePanel(tabbedPane).addToBottom(notificationContainer)); Disposer.register(parentDisposable, tabPanel); } @@ -102,7 +145,10 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { return tabbedPane; } - public StandardChatToolWindowTabbedPane getChatTabbedPane() { - return tabbedPane; + public void clearSelectedFilesNotification(Project project) { + project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); + project.getMessageBus() + .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded(emptyList()); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java index ef340acf..5b8645cf 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java @@ -23,7 +23,7 @@ public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel { public StandardChatToolWindowTabPanel( @NotNull Project project, @NotNull Conversation conversation) { - super(project, conversation, false); + super(project, conversation); if (conversation.getMessages().isEmpty()) { displayLandingView(); } else { @@ -67,8 +67,14 @@ public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel { } messageResponseBody.hideCaret(); + var userMessagePanel = new UserMessagePanel(project, message, this); + var imageFilePath = message.getImageFilePath(); + if (imageFilePath != null && !imageFilePath.isEmpty()) { + userMessagePanel.displayImage(imageFilePath); + } + var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(new UserMessagePanel(project, message, this)); + messagePanel.add(userMessagePanel); messagePanel.add(new ResponsePanel() .withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT)) .withDeleteAction(() -> removeMessage(message.getId(), conversation)) diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java new file mode 100644 index 00000000..250be68e --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java @@ -0,0 +1,86 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import static com.intellij.util.ui.JBUI.Panels.simplePanel; + +import com.intellij.icons.AllIcons.General; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTBundle; +import java.awt.BorderLayout; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.event.ItemEvent; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; + +public class ImageAccordion extends JPanel { + + public ImageAccordion(String fileName, byte[] imageData) { + super(new BorderLayout()); + setOpaque(false); + + var contentPanel = createContentPanel(fileName, imageData); + add(createToggleButton(contentPanel), BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + } + + private JPanel createContentPanel(String fileName, byte[] imageData) { + var panel = new JPanel(); + panel.setOpaque(false); + panel.setVisible(true); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(JBUI.Borders.empty(4, 0)); + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData); + BufferedImage originalImage = ImageIO.read(inputStream); + int maxHeight = 80; + BufferedImage resizedImage = resizeImage(originalImage, maxHeight); + panel.add(simplePanel() + .andTransparent() + .addToTop( + new JBLabel("%s".formatted(fileName)) + .withBorder(JBUI.Borders.emptyBottom(4))) + .addToLeft(new JBLabel(new ImageIcon(resizedImage))), BorderLayout.LINE_START); + } catch (IOException e) { + panel.add(new JBLabel("ERROR: Something went wrong while reading the image")); + throw new RuntimeException(e); + } + return panel; + } + + private JToggleButton createToggleButton(JPanel contentPane) { + var accordionToggle = new JToggleButton( + CodeGPTBundle.get("imageAccordion.title"), + General.ArrowDown); + accordionToggle.setFocusPainted(false); + accordionToggle.setContentAreaFilled(false); + accordionToggle.setBackground(getBackground()); + accordionToggle.setSelectedIcon(General.ArrowUp); + accordionToggle.setBorder(null); + accordionToggle.setSelected(true); + accordionToggle.setHorizontalAlignment(SwingConstants.LEADING); + accordionToggle.setHorizontalTextPosition(SwingConstants.LEADING); + accordionToggle.addItemListener(e -> + contentPane.setVisible(e.getStateChange() == ItemEvent.SELECTED)); + return accordionToggle; + } + + private BufferedImage resizeImage(BufferedImage originalImage, int maxHeight) { + double aspectRatio = (double) originalImage.getWidth() / originalImage.getHeight(); + int newWidth = (int) (maxHeight * aspectRatio); + Image resizedImage = originalImage.getScaledInstance(newWidth, maxHeight, Image.SCALE_SMOOTH); + BufferedImage bufferedResizedImage = new BufferedImage(newWidth, maxHeight, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = bufferedResizedImage.createGraphics(); + g2d.drawImage(resizedImage, 0, 0, null); + g2d.dispose(); + return bufferedResizedImage; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java deleted file mode 100644 index 0b0072b2..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java +++ /dev/null @@ -1,86 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.ui; - -import static java.lang.String.format; -import static java.util.Collections.emptyList; - -import com.intellij.icons.AllIcons.General; -import com.intellij.openapi.project.Project; -import com.intellij.ui.JBColor; -import com.intellij.ui.components.ActionLink; -import com.intellij.ui.components.JBLabel; -import com.intellij.util.ui.JBUI; -import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo; -import ee.carlrobert.codegpt.CodeGPTKeys; -import ee.carlrobert.codegpt.ReferencedFile; -import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; -import java.awt.BorderLayout; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import javax.swing.JPanel; -import javax.swing.SwingConstants; -import org.jetbrains.annotations.NotNull; - -public class SelectedFilesNotification extends JPanel { - - private final Project project; - private final JBLabel label; - - public SelectedFilesNotification(@NotNull Project project) { - super(new BorderLayout()); - this.project = project; - this.label = new JBLabel( - getSelectedFilesLabel(), - General.BalloonInformation, - SwingConstants.LEADING); - - setVisible(false); - setBorder(JBUI.Borders.compound( - JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), - JBUI.Borders.empty(8, 12))); - - setBackground(NotificationInfo.backgroundColor()); - setForeground(NotificationInfo.foregroundColor()); - add(label, BorderLayout.LINE_START); - add(new ActionLink("Remove", (event) -> { - clearSelectedFilesNotification(); - }), BorderLayout.LINE_END); - } - - public void displaySelectedFilesNotification(@NotNull List referencedFiles) { - if (referencedFiles.isEmpty()) { - return; - } - - label.setText(referencedFiles.size() + " files selected"); - var referencedFilePaths = referencedFiles.stream() - .map(ReferencedFile::getFilePath) - .collect(Collectors.toList()); - label.setToolTipText(getHtml(referencedFilePaths)); - setVisible(true); - } - - public void clearSelectedFilesNotification() { - project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); - project.getMessageBus() - .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) - .filesIncluded(emptyList()); - - label.setText("0 files selected"); - label.setToolTipText(null); - setVisible(false); - } - - private String getHtml(List referencedFilePaths) { - var html = referencedFilePaths.stream() - .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) - .collect(Collectors.joining()); - return format("
      %s
    ", html); - } - - private String getSelectedFilesLabel() { - var selectedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); - var fileCount = selectedFiles == null ? 0 : selectedFiles.size(); - return fileCount + " files selected"; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java new file mode 100644 index 00000000..52bcc66f --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java @@ -0,0 +1,50 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import com.intellij.icons.AllIcons.General; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.ActionLink; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo; +import java.awt.BorderLayout; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +public class ToolWindowFooterNotification extends JPanel { + + private final JBLabel label; + + public ToolWindowFooterNotification(Runnable onRemove) { + this("", onRemove); + } + + public ToolWindowFooterNotification(String text, Runnable onRemove) { + super(new BorderLayout()); + this.label = new JBLabel(text, General.BalloonInformation, SwingConstants.LEADING); + + setVisible(false); + setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), + JBUI.Borders.empty(8, 12))); + + setBackground(NotificationInfo.backgroundColor()); + setForeground(NotificationInfo.foregroundColor()); + add(label, BorderLayout.LINE_START); + add(new ActionLink("Remove", (event) -> { + hideNotification(); + onRemove.run(); + }), BorderLayout.LINE_END); + } + + public void show(String text, String toolTipText) { + label.setText(text); + label.setToolTipText(toolTipText); + setVisible(true); + } + + public void hideNotification() { + label.setText(""); + label.setToolTipText(null); + setVisible(false); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java index 5e723ed4..5db6de56 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui; +import com.intellij.icons.AllIcons.General; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.ui.ColorUtil; @@ -11,6 +12,9 @@ import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.GeneralSettings; import java.awt.BorderLayout; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import javax.swing.JPanel; import javax.swing.SwingConstants; @@ -39,6 +43,18 @@ public class UserMessagePanel extends JPanel { } } + public void displayImage(String imageFilePath) { + try { + var path = Paths.get(imageFilePath); + add(new ImageAccordion(path.getFileName().toString(), Files.readAllBytes(path))); + } catch (IOException e) { + add(new JBLabel( + "Unable to load image %s".formatted(imageFilePath), + General.Error, + SwingConstants.LEFT)); + } + } + private ChatMessageResponseBody createResponseBody( Project project, String prompt, diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java new file mode 100644 index 00000000..77acddf2 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java @@ -0,0 +1,11 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; + +import com.intellij.util.messages.Topic; + +public interface AttachImageNotifier { + + Topic IMAGE_ATTACHMENT_FILE_PATH_TOPIC = + Topic.create("imageAttachmentFilePath", AttachImageNotifier.class); + + void imageAttached(String filePath); +} 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 d7f11dc6..1d1b14b5 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 @@ -18,8 +18,6 @@ import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.llama.LlamaModel; import ee.carlrobert.codegpt.completions.you.YouUserManager; import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; -import ee.carlrobert.codegpt.conversations.ConversationService; -import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.GeneralSettingsState; import ee.carlrobert.codegpt.settings.service.ServiceType; @@ -39,13 +37,13 @@ import org.jetbrains.annotations.NotNull; public class ModelComboBoxAction extends ComboBoxAction { - private final Runnable onAddNewTab; + private final Runnable onModelChange; private final GeneralSettingsState settings; private final OpenAISettingsState openAISettings; private final YouSettingsState youSettings; - public ModelComboBoxAction(Runnable onAddNewTab, ServiceType selectedService) { - this.onAddNewTab = onAddNewTab; + public ModelComboBoxAction(Runnable onModelChange, ServiceType selectedService) { + this.onModelChange = onModelChange; settings = GeneralSettings.getCurrentState(); openAISettings = OpenAISettings.getCurrentState(); youSettings = YouSettings.getCurrentState(); @@ -74,6 +72,7 @@ public class ModelComboBoxAction extends ComboBoxAction { var actionGroup = new DefaultActionGroup(); actionGroup.addSeparator("OpenAI"); List.of( + OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW, OpenAIChatCompletionModel.GPT_4_0125_128k, OpenAIChatCompletionModel.GPT_3_5_0125_16k, OpenAIChatCompletionModel.GPT_4_32k, @@ -210,7 +209,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - handleProviderChange(serviceType, label, icon, comboBoxPresentation); + handleModelChange(serviceType, label, icon, comboBoxPresentation); } @Override @@ -220,7 +219,7 @@ public class ModelComboBoxAction extends ComboBoxAction { }; } - private void handleProviderChange( + private void handleModelChange( ServiceType serviceType, String label, Icon icon, @@ -228,13 +227,7 @@ public class ModelComboBoxAction extends ComboBoxAction { settings.setSelectedService(serviceType); comboBoxPresentation.setIcon(icon); comboBoxPresentation.setText(label); - - var currentConversation = ConversationsState.getCurrentConversation(); - if (currentConversation != null && !currentConversation.getMessages().isEmpty()) { - onAddNewTab.run(); - } else { - ConversationService.getInstance().startConversation(); - } + onModelChange.run(); } private AnAction createOpenAIModelAction( @@ -252,7 +245,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { openAISettings.setModel(model.getCode()); - handleProviderChange( + handleModelChange( OPENAI, model.getDescription(), Icons.OpenAI, @@ -281,7 +274,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { youSettings.setChatMode(mode); - handleProviderChange( + handleModelChange( YOU, mode.getDescription(), Icons.YouSmall, @@ -311,7 +304,7 @@ public class ModelComboBoxAction extends ComboBoxAction { public void actionPerformed(@NotNull AnActionEvent e) { youSettings.setCustomModel(model); youSettings.setChatMode(YouCompletionMode.CUSTOM); - handleProviderChange( + handleModelChange( YOU, model.getDescription(), Icons.YouSmall, diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index aca839b6..38a62f4a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -1,6 +1,12 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; +import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; +import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW; + import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.util.registry.Registry; @@ -10,11 +16,14 @@ import com.intellij.ui.components.JBTextArea; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.Icons; +import ee.carlrobert.codegpt.actions.AttachImageAction; import ee.carlrobert.codegpt.completions.CompletionRequestHandler; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import ee.carlrobert.codegpt.ui.IconActionButton; import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.BasicStroke; import java.awt.BorderLayout; -import java.awt.Cursor; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; @@ -23,16 +32,14 @@ import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import javax.swing.AbstractAction; -import javax.swing.Icon; -import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class UserPromptTextArea extends JPanel { @@ -41,11 +48,12 @@ public class UserPromptTextArea extends JPanel { private static final JBColor BACKGROUND_COLOR = JBColor.namedColor( "Editor.SearchField.background", com.intellij.util.ui.UIUtil.getTextFieldBackground()); + private final AtomicReference requestHandlerRef = + new AtomicReference<>(); private final JBTextArea textArea; - private final int textAreaRadius = 16; private final Consumer onSubmit; - private JButton stopButton; + private IconActionButton stopButton; private JPanel iconsPanel; private boolean submitEnabled = true; @@ -150,6 +158,10 @@ public class UserPromptTextArea extends JPanel { stopButton.setEnabled(!submitEnabled); } + public void setRequestHandler(@NotNull CompletionRequestHandler handler) { + requestHandlerRef.set(handler); + } + private void handleSubmit() { if (submitEnabled && !textArea.getText().isEmpty()) { // Replacing each newline with two newlines to ensure proper Markdown formatting @@ -163,12 +175,34 @@ public class UserPromptTextArea extends JPanel { setOpaque(false); add(textArea, BorderLayout.CENTER); - stopButton = createIconButton(AllIcons.Actions.Suspend, null); + stopButton = new IconActionButton( + new AnAction("Stop", "Stop current inference", AllIcons.Actions.Suspend) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + var handler = requestHandlerRef.get(); + if (handler != null) { + handler.cancel(); + } + } + }); + stopButton.setEnabled(false); var flowLayout = new FlowLayout(FlowLayout.RIGHT); flowLayout.setHgap(8); iconsPanel = new JPanel(flowLayout); - iconsPanel.add(createIconButton(Icons.Send, this::handleSubmit)); + iconsPanel.add(new IconActionButton( + new AnAction("Send Message", "Send message", Icons.Send) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + handleSubmit(); + } + })); + var selectedService = GeneralSettings.getCurrentState().getSelectedService(); + if (selectedService == ANTHROPIC + || (selectedService == OPENAI + && GPT_4_VISION_PREVIEW.getCode().equals(OpenAISettings.getCurrentState().getModel()))) { + iconsPanel.add(new IconActionButton(new AttachImageAction())); + } iconsPanel.add(stopButton); add(iconsPanel, BorderLayout.EAST); } @@ -180,19 +214,4 @@ public class UserPromptTextArea extends JPanel { textArea.setFont(UIManager.getFont("TextField.font")); } } - - // TODO: IconActionButton? - private JButton createIconButton(Icon icon, @Nullable Runnable submitListener) { - var button = UIUtil.createIconButton(icon); - if (submitListener != null) { - button.addActionListener((e) -> handleSubmit()); - } - button.setCursor(new Cursor(Cursor.HAND_CURSOR)); - button.setEnabled(false); - return button; - } - - public void setRequestHandler(@NotNull CompletionRequestHandler requestService) { - stopButton.addActionListener(e -> requestService.cancel()); - } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java index d350d857..0c5374f2 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java @@ -1,12 +1,7 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import com.intellij.openapi.actionSystem.ActionPlaces; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.ui.components.JBCheckBox; -import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier; -import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; import ee.carlrobert.codegpt.settings.service.ServiceType; import java.awt.BorderLayout; import javax.swing.JPanel; @@ -16,7 +11,7 @@ public class UserPromptTextAreaHeader extends JPanel { public UserPromptTextAreaHeader( ServiceType selectedService, TotalTokensPanel totalTokensPanel, - Runnable onAddNewTab) { + Runnable onModelChange) { super(new BorderLayout()); setOpaque(false); setBorder(JBUI.Borders.emptyBottom(8)); @@ -29,29 +24,7 @@ public class UserPromptTextAreaHeader extends JPanel { break; default: } - add(new ModelComboBoxAction(onAddNewTab, selectedService) + add(new ModelComboBoxAction(onModelChange, selectedService) .createCustomComponent(ActionPlaces.UNKNOWN), BorderLayout.LINE_END); } - - private void subscribeToYouTopics(JBCheckBox gpt4CheckBox) { - var messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); - subscribeToYouSubscriptionTopic(messageBusConnection, gpt4CheckBox); - subscribeToSignedOutTopic(messageBusConnection, gpt4CheckBox); - } - - private void subscribeToSignedOutTopic( - MessageBusConnection messageBusConnection, - JBCheckBox gpt4CheckBox) { - messageBusConnection.subscribe( - SignedOutNotifier.SIGNED_OUT_TOPIC, - (SignedOutNotifier) () -> gpt4CheckBox.setEnabled(false)); - } - - private void subscribeToYouSubscriptionTopic( - MessageBusConnection messageBusConnection, - JBCheckBox gpt4CheckBox) { - messageBusConnection.subscribe( - YouSubscriptionNotifier.SUBSCRIPTION_TOPIC, - (YouSubscriptionNotifier) () -> gpt4CheckBox.setEnabled(true)); - } } diff --git a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java b/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java index 1ec5746d..9a898f08 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java @@ -141,6 +141,15 @@ public class FileUtil { } } + public static String getImageMediaType(String fileName) { + var fileExtension = getFileExtension(fileName); + return switch (fileExtension) { + case "png" -> "image/png"; + case "jpg", "jpeg" -> "image/jpeg"; + default -> throw new IllegalArgumentException("Unsupported image type: " + fileExtension); + }; + } + public static String getResourceContent(String name) { try (var stream = Objects.requireNonNull(FileUtil.class.getResourceAsStream(name))) { return new String(stream.readAllBytes(), StandardCharsets.UTF_8); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt index f8cfc21e..13317571 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt @@ -1,9 +1,11 @@ package ee.carlrobert.codegpt +import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.util.Disposer import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil import ee.carlrobert.codegpt.completions.you.YouUserManager import ee.carlrobert.codegpt.completions.you.auth.AuthenticationHandler @@ -13,8 +15,11 @@ import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResp 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.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.service.you.YouSettings +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier import ee.carlrobert.codegpt.ui.OverlayUtil +import java.nio.file.Paths class CodeGPTProjectActivity : ProjectActivity { @@ -23,12 +28,24 @@ class CodeGPTProjectActivity : ProjectActivity { CredentialsStore.loadAll() if (YouUserManager.getInstance().authenticationResponse == null) { - ApplicationManager.getApplication() - .executeOnPooledThread { this.handleYouServiceAuthentication() } + handleYouServiceAuthenticationAsync() + } + + if (!ApplicationManager.getApplication().isUnitTestMode + && ConfigurationSettings.getCurrentState().isCheckForNewScreenshots + ) { + val pathToWatch = Paths.get(System.getProperty("user.home"), "Desktop") + val fileWatcher = FileWatcher(pathToWatch) + fileWatcher.watch { + if (listOf("jpg", "jpeg", "png").contains(it.extension)) { + showImageAttachmentNotification(project, it.absolutePath) + } + } + Disposer.register(project, fileWatcher) } } - private fun handleYouServiceAuthentication() { + private fun handleYouServiceAuthenticationAsync() { val settings = YouSettings.getCurrentState() val password = getCredential(CredentialKey.YOU_ACCOUNT_PASSWORD) if (settings.email.isNotEmpty() && !password.isNullOrEmpty()) { @@ -57,4 +74,27 @@ class CodeGPTProjectActivity : ProjectActivity { }) } } + + private fun showImageAttachmentNotification(project: Project, filePath: String) { + OverlayUtil.getDefaultNotification( + CodeGPTBundle.get("imageAttachmentNotification.content"), + NotificationType.INFORMATION + ) + .addAction(NotificationAction.createSimpleExpiring( + CodeGPTBundle.get("imageAttachmentNotification.action") + ) { + CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.set(project, filePath) + project.messageBus + .syncPublisher( + AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC + ) + .imageAttached(filePath) + }) + .addAction(NotificationAction.createSimpleExpiring( + CodeGPTBundle.get("shared.notification.doNotShowAgain") + ) { + ConfigurationSettings.getCurrentState().isCheckForNewScreenshots = false + }) + .notify(project) + } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt index af31c937..505c5bea 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt; +package ee.carlrobert.codegpt import com.intellij.ide.plugins.InstalledPluginsState import com.intellij.notification.NotificationAction @@ -39,7 +39,7 @@ class CodeGPTUpdateActivity : ProjectActivity { Task.Backgroundable(project, CodeGPTBundle.get("checkForUpdatesTask.title"), true) { override fun run(indicator: ProgressIndicator) { val isLatestVersion = - !InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID); + !InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID) if (project.isDisposed || isLatestVersion) { return } @@ -55,7 +55,7 @@ class CodeGPTUpdateActivity : ProjectActivity { .executeOnPooledThread { installCodeGPTUpdate(project) } }) .addAction(NotificationAction.createSimpleExpiring( - CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton") + CodeGPTBundle.get("shared.notification.doNotShowAgain") ) { ConfigurationSettings.getCurrentState().isCheckForPluginUpdates = false }) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt new file mode 100644 index 00000000..d1c20669 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt @@ -0,0 +1,29 @@ +package ee.carlrobert.codegpt + +import com.intellij.openapi.Disposable +import org.apache.commons.io.monitor.FileAlterationListenerAdaptor +import org.apache.commons.io.monitor.FileAlterationMonitor +import org.apache.commons.io.monitor.FileAlterationObserver +import java.io.File +import java.nio.file.Path + +class FileWatcher(private val pathToWatch: Path) : Disposable { + + private val fileMonitor = + FileAlterationMonitor(500, FileAlterationObserver(pathToWatch.toFile())) + + fun watch(onFileCreated: (File) -> Unit) { + val observer = FileAlterationObserver(pathToWatch.toFile()) + observer.addListener(object : FileAlterationListenerAdaptor() { + override fun onFileCreate(file: File) { + onFileCreated(file) + } + }) + fileMonitor.addObserver(observer) + fileMonitor.start() + } + + override fun dispose() { + fileMonitor.stop() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt new file mode 100644 index 00000000..509ed8c2 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt @@ -0,0 +1,40 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier + +class AttachImageAction : AnAction( + CodeGPTBundle.get("action.attachImage"), + CodeGPTBundle.get("action.attachImageDescription"), + Icons.Upload +) { + + override fun actionPerformed(e: AnActionEvent) { + FileChooser.chooseFiles(createSingleImageFileDescriptor(), e.project, null).also { files -> + if (files.isNotEmpty()) { + check(files.size == 1) { "Expected exactly one file to be selected" } + e.project?.let { project -> + CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH[project] = files.first().path + project.messageBus + .syncPublisher(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC) + .imageAttached(files.first().path) + } + } + } + } + + private fun createSingleImageFileDescriptor() = FileChooserDescriptor( + true, false, false, false, false, false + ).apply { + withFileFilter { file -> + file.extension in listOf("jpg", "jpeg", "png") + } + withTitle(CodeGPTBundle.get("imageFileChooser.title")) + } +} \ No newline at end of file diff --git a/src/main/resources/icons/send.svg b/src/main/resources/icons/send.svg index 89cf3907..b6101d19 100644 --- a/src/main/resources/icons/send.svg +++ b/src/main/resources/icons/send.svg @@ -1,6 +1,6 @@ - + diff --git a/src/main/resources/icons/send_dark.svg b/src/main/resources/icons/send_dark.svg index 5dd4ba3b..d3d497c5 100644 --- a/src/main/resources/icons/send_dark.svg +++ b/src/main/resources/icons/send_dark.svg @@ -1,6 +1,6 @@ - + diff --git a/src/main/resources/icons/upload.svg b/src/main/resources/icons/upload.svg new file mode 100644 index 00000000..b26837f5 --- /dev/null +++ b/src/main/resources/icons/upload.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/upload_dark.svg b/src/main/resources/icons/upload_dark.svg new file mode 100644 index 00000000..2241155e --- /dev/null +++ b/src/main/resources/icons/upload_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index dc6c02ca..b5858202 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -87,6 +87,7 @@ configurationConfigurable.table.header.promptColumnLabel=Prompt configurationConfigurable.table.action.revertToDefaults.text=Revert to Defaults configurationConfigurable.table.action.addKeymap.text=Add Shortcut configurationConfigurable.checkForPluginUpdates.label=Check for plugin updates automatically +configurationConfigurable.checkForNewScreenshots.label=Check for new screenshots automatically configurationConfigurable.openNewTabCheckBox.label=Open a new chat on each action configurationConfigurable.enableMethodNameGeneration.label=Enable method name lookup suggestions configurationConfigurable.autoFormatting.label=Enable automatic code formatting @@ -178,7 +179,6 @@ validation.error.mustBeGreaterThanZero=Value must be greater than 0 checkForUpdatesTask.title=Checking for CodeGPT update... checkForUpdatesTask.notification.message=An update for CodeGPT is available. checkForUpdatesTask.notification.installButton=Install update -checkForUpdatesTask.notification.hideButton=Do not show again llamaServerAgent.buildingProject.description=Building llama.cpp... llamaServerAgent.serverBootup.description=Booting up server... notification.compilationError.description=CodeGPT has detected a compilation error. Would you like assistance in resolving it? @@ -190,4 +190,11 @@ shared.infillPromptTemplate=Infill template: shared.apiVersion=API version: shared.configuration=Configuration shared.port=Port: -codeCompletion.progress.title=Code completion in progress \ No newline at end of file +shared.notification.doNotShowAgain=Do not show again +codeCompletion.progress.title=Code completion in progress +imageAttachmentNotification.content=New image detected on desktop. Would you like to attach it to your current conversation? +imageAttachmentNotification.action=Attach image +action.attachImage=Attach Image +action.attachImageDescription=Attach an image +imageFileChooser.title=Select Image +imageAccordion.title=Attached image diff --git a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java index c09a7262..0b039e06 100644 --- a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java +++ b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java @@ -39,9 +39,6 @@ public class CodeCompletionServiceTest extends IntegrationTest { myFixture.type('c'); - PlatformTestUtil.waitWithEventsDispatching( - "Editor inlay assertions failed", - () -> "TEST_OUTPUT".equals(PREVIOUS_INLAY_TEXT.get(myFixture.getEditor())), - 5); + waitExpecting(() -> "TEST_OUTPUT".equals(PREVIOUS_INLAY_TEXT.get(myFixture.getEditor()))); } } diff --git a/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java b/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java index 5e1900d8..18fa3020 100644 --- a/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java +++ b/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java @@ -6,10 +6,8 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e; import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.conversations.ConversationService; @@ -50,7 +48,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testAzureChatCompletionCall() { @@ -86,7 +84,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testYouChatCompletionCall() { @@ -137,7 +135,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testLlamaChatCompletionCall() { @@ -171,7 +169,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } private CompletionResponseEventListener getRequestEventListener(Message message) { diff --git a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java index a7ec0ed0..3aba409d 100644 --- a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java +++ b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java @@ -7,10 +7,9 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e; import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse; -import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.Objects.requireNonNull; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; @@ -23,6 +22,10 @@ import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel; import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; import java.util.List; import java.util.Map; import testsupport.IntegrationTest; @@ -57,11 +60,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -114,23 +116,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of( Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT), Map.of("role", "user", "content", - "Use the following context to answer question at the end:\n\n" - + "File Path: TEST_FILE_PATH_1\n" - + "File Content:\n" - + "```TEST_FILE_NAME_1\n" - + "TEST_FILE_CONTENT_1\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_2\n" - + "File Content:\n" - + "```TEST_FILE_NAME_2\n" - + "TEST_FILE_CONTENT_2\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_3\n" - + "File Content:\n" - + "```TEST_FILE_NAME_3\n" - + "TEST_FILE_CONTENT_3\n" - + "```\n\n" - + "Question: TEST_MESSAGE"))); + """ + Use the following context to answer question at the end: + + File Path: TEST_FILE_PATH_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: TEST_FILE_PATH_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: TEST_FILE_PATH_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE"""))); return List.of( jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), @@ -140,11 +147,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -175,6 +181,79 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); } + public void testSendingOpenAIMessageWithImage() { + var testImagePath = requireNonNull(getClass().getResource("/images/test-image.png")).getPath(); + getProject().putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, testImagePath); + useOpenAIService("gpt-4-vision-preview"); + ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT); + var message = new Message("TEST_MESSAGE"); + var conversation = ConversationService.getInstance().startConversation(); + var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + expectOpenAI((StreamHttpExchange) request -> { + assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeaders().get(AUTHORIZATION).get(0)).isEqualTo("Bearer TEST_API_KEY"); + try { + var testImageUrl = "data:image/png;base64," + + Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of(testImagePath))); + assertThat(request.getBody()) + .extracting("model", "messages") + .containsExactly( + "gpt-4-vision-preview", + List.of( + Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT), + Map.of("role", "user", "content", List.of( + Map.of( + "type", "image_url", + "image_url", Map.of("url", testImageUrl)), + Map.of("type", "text", "text", "TEST_MESSAGE") + )))); + } catch (IOException e) { + throw new RuntimeException(e); + } + return List.of( + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!"))))); + }); + + panel.sendMessage(message); + + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); + var encodingManager = EncodingManager.getInstance(); + assertThat(panel.getTokenDetails()).extracting( + "systemPromptTokens", + "conversationTokens", + "userPromptTokens", + "highlightedTokens") + .containsExactly( + encodingManager.countTokens(COMPLETION_SYSTEM_PROMPT), + encodingManager.countTokens(message.getPrompt()), + 0, + 0); + assertThat(panel.getConversation()) + .isNotNull() + .extracting("id", "model", "clientCode", "discardTokenLimit") + .containsExactly( + conversation.getId(), + conversation.getModel(), + conversation.getClientCode(), + false); + var messages = panel.getConversation().getMessages(); + assertThat(messages.size()).isOne(); + assertThat(messages.get(0)) + .extracting("id", "prompt", "response", "imageFilePath") + .containsExactly( + message.getId(), + message.getPrompt(), + message.getResponse(), + message.getImageFilePath()); + } + public void testFixCompileErrorsWithOpenAIService() { getProject().putUserData(CodeGPTKeys.SELECTED_FILES, List.of( new ReferencedFile("TEST_FILE_NAME_1", "TEST_FILE_PATH_1", "TEST_FILE_CONTENT_1"), @@ -201,23 +280,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of( Map.of("role", "system", "content", FIX_COMPILE_ERRORS_SYSTEM_PROMPT), Map.of("role", "user", "content", - "Use the following context to answer question at the end:\n\n" - + "File Path: TEST_FILE_PATH_1\n" - + "File Content:\n" - + "```TEST_FILE_NAME_1\n" - + "TEST_FILE_CONTENT_1\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_2\n" - + "File Content:\n" - + "```TEST_FILE_NAME_2\n" - + "TEST_FILE_CONTENT_2\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_3\n" - + "File Content:\n" - + "```TEST_FILE_NAME_3\n" - + "TEST_FILE_CONTENT_3\n" - + "```\n\n" - + "Question: TEST_MESSAGE"))); + """ + Use the following context to answer question at the end: + + File Path: TEST_FILE_PATH_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: TEST_FILE_PATH_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: TEST_FILE_PATH_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE"""))); return List.of( jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), @@ -227,11 +311,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message, ConversationType.FIX_COMPILE_ERRORS); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -312,11 +395,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message, ConversationType.DEFAULT); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); assertThat(panel.getConversation()) .isNotNull() .extracting("id", "model", "clientCode", "discardTokenLimit") diff --git a/src/test/java/testsupport/IntegrationTest.java b/src/test/java/testsupport/IntegrationTest.java index 8470f8d1..f92be8e2 100644 --- a/src/test/java/testsupport/IntegrationTest.java +++ b/src/test/java/testsupport/IntegrationTest.java @@ -1,5 +1,6 @@ package testsupport; +import com.intellij.openapi.util.Key; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.llm.client.mixin.ExternalServiceTestMixin; @@ -17,8 +18,17 @@ public class IntegrationTest extends BasePlatformTestCase implements @Override protected void tearDown() throws Exception { ExternalServiceTestMixin.clearAll(); - getProject().putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList()); - getProject().putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, ""); + clearKeys(); super.tearDown(); } + + private void clearKeys() { + putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList()); + putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, ""); + putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""); + } + + private void putUserData(Key key, T value) { + getProject().putUserData(key, value); + } } diff --git a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java index c6f19be6..9c7d4aa2 100644 --- a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java +++ b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java @@ -3,19 +3,25 @@ package testsupport.mixin; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY; +import com.intellij.testFramework.PlatformTestUtil; import ee.carlrobert.codegpt.credentials.CredentialsStore; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import java.util.function.BooleanSupplier; public interface ShortcutsTestMixin { default void useOpenAIService() { + useOpenAIService("gpt-4"); + } + + default void useOpenAIService(String model) { GeneralSettings.getCurrentState().setSelectedService(ServiceType.OPENAI); CredentialsStore.INSTANCE.setCredential(OPENAI_API_KEY, "TEST_API_KEY"); - OpenAISettings.getCurrentState().setModel("gpt-4"); + OpenAISettings.getCurrentState().setModel(model); } default void useAzureService() { @@ -35,4 +41,11 @@ public interface ShortcutsTestMixin { GeneralSettings.getCurrentState().setSelectedService(ServiceType.LLAMA_CPP); LlamaSettings.getCurrentState().setServerPort(null); } + + default void waitExpecting(BooleanSupplier condition) { + PlatformTestUtil.waitWithEventsDispatching( + "Waiting for message response timed out or did not meet expected conditions", + condition, + 5); + } } diff --git a/src/test/resources/images/test-image.png b/src/test/resources/images/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed28a16f602cae345abc793e28cd4efec329da7 GIT binary patch literal 3940 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 literal 0 HcmV?d00001 From 35ee02ba7928a4bdb6f0316dc002f3af8a3e46d4 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 2 Apr 2024 20:43:26 +0300 Subject: [PATCH 02/20] feat: display total tokens for all providers (closes #397) --- .../chat/ChatToolWindowTabPanel.java | 18 +++++++++-- .../chat/ui/textarea/TotalTokensPanel.java | 20 ++++++++++++- .../ui/textarea/UserPromptTextAreaHeader.java | 30 ------------------- 3 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index a3a13784..3a9194b9 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -6,11 +6,13 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; import com.intellij.ui.JBColor; import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.JBUI.Borders; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.ReferencedFile; @@ -30,10 +32,10 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea; -import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextAreaHeader; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.BorderLayout; @@ -267,9 +269,8 @@ public abstract class ChatToolWindowTabPanel implements Disposable { JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), JBUI.Borders.empty(8))); var contentManager = project.getService(StandardChatToolWindowContentManager.class); - panel.add(JBUI.Panels.simplePanel(new UserPromptTextAreaHeader( + panel.add(JBUI.Panels.simplePanel(createUserPromptTextAreaHeader( selectedService, - totalTokensPanel, () -> { ConversationService.getInstance().startConversation(); contentManager.createNewTabPanel(); @@ -278,6 +279,17 @@ public abstract class ChatToolWindowTabPanel implements Disposable { return panel; } + private JPanel createUserPromptTextAreaHeader( + ServiceType selectedService, + Runnable onModelChange) { + return JBUI.Panels.simplePanel() + .withBorder(Borders.emptyBottom(8)) + .andTransparent() + .addToLeft(totalTokensPanel) + .addToRight(new ModelComboBoxAction(onModelChange, selectedService) + .createCustomComponent(ActionPlaces.UNKNOWN)); + } + private JPanel createRootPanel() { var gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index 88763678..c9b0da5c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -17,6 +17,8 @@ import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.conversations.Conversation; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.ServiceType; import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -154,12 +156,28 @@ public class TotalTokensPanel extends JPanel { entry.getKey(), entry.getValue())) .collect(Collectors.joining()); - iconLabel.setToolTipText("" + html + ""); + iconLabel.setToolTipText(getIconToolTipText(html)); } }); return iconLabel; } + private String getIconToolTipText(String html) { + if (GeneralSettings.getCurrentState().getSelectedService() != ServiceType.OPENAI) { + return """ + + + ⓘ Keep in mind that the output values might vary across different + large language models due to variations in their encoding methods. + +

    + %s + """.formatted(html); + } + return ""; + } + private String getLabelHtml(int total) { return format("Tokens: %d", total); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java deleted file mode 100644 index 0c5374f2..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java +++ /dev/null @@ -1,30 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; - -import com.intellij.openapi.actionSystem.ActionPlaces; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.settings.service.ServiceType; -import java.awt.BorderLayout; -import javax.swing.JPanel; - -public class UserPromptTextAreaHeader extends JPanel { - - public UserPromptTextAreaHeader( - ServiceType selectedService, - TotalTokensPanel totalTokensPanel, - Runnable onModelChange) { - super(new BorderLayout()); - setOpaque(false); - setBorder(JBUI.Borders.emptyBottom(8)); - switch (selectedService) { - case OPENAI: - case AZURE: - add(totalTokensPanel, BorderLayout.LINE_START); - break; - case YOU: - break; - default: - } - add(new ModelComboBoxAction(onModelChange, selectedService) - .createCustomComponent(ActionPlaces.UNKNOWN), BorderLayout.LINE_END); - } -} From 79ef7550fe0ed0ac2123ccd87f9546b184b98ca1 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 2 Apr 2024 20:54:59 +0300 Subject: [PATCH 03/20] fix: send button enabled state --- .../toolwindow/chat/ui/textarea/UserPromptTextArea.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index 38a62f4a..fe2e302f 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -90,12 +90,6 @@ public class UserPromptTextArea extends JPanel { UserPromptTextArea.super.paintBorder(UserPromptTextArea.super.getGraphics()); } }); - textArea.getDocument().addDocumentListener(new DocumentAdapter() { - @Override - protected void textChanged(@NotNull DocumentEvent e) { - iconsPanel.getComponents()[0].setEnabled(e.getDocument().getLength() > 0); - } - }); updateFont(); init(); } From 2b98b65210b33a81094baf13043df8641ac77e49 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 2 Apr 2024 20:59:22 +0300 Subject: [PATCH 04/20] docs: update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dab7732f..8bf1dc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models +- Total token panel for all providers + +### Fixed + +- A couple of IntelliJ Platform errors/warnings +- Error when adding a single file to the context ### Removed From f0172722c75ae50d2ea895f68cbef0c90bbbcc7f Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 3 Apr 2024 01:04:22 +0300 Subject: [PATCH 05/20] feat: add support for configuring code completions via settings --- CHANGELOG.md | 1 + .../CodeCompletionEnabledListener.java | 26 --------- .../actions/DisableCompletionsAction.java | 41 -------------- .../actions/EnableCompletionsAction.java | 41 -------------- .../configuration/ConfigurationComponent.java | 2 - .../configuration/ConfigurationState.java | 13 +---- .../service/llama/LlamaSettingsState.java | 25 ++++++++- .../service/llama/form/LlamaSettingsForm.java | 11 ++++ .../service/openai/OpenAISettings.java | 1 + .../service/openai/OpenAISettingsForm.java | 11 ++++ .../service/openai/OpenAISettingsState.java | 25 ++++++++- .../CodeCompletionFeatureToggleActions.kt | 54 +++++++++++++++++++ .../CodeCompletionRequestFactory.kt | 14 ++--- .../CodeGPTInlineCompletionProvider.kt | 14 +++-- .../CodeCompletionConfigurationForm.kt | 49 +++++++++++++++++ .../resources/messages/codegpt.properties | 4 ++ .../CodeCompletionServiceTest.java | 5 +- 17 files changed, 198 insertions(+), 139 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf1dc91..00c930b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models - Total token panel for all providers +- Support for configuring code completions via settings ### Fixed diff --git a/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java b/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java deleted file mode 100644 index 1e8a855c..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java +++ /dev/null @@ -1,26 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import com.intellij.util.messages.Topic; -import com.intellij.util.messages.Topic.BroadcastDirection; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; -import java.util.EventListener; - -/** - * {@link EventListener} for changes of {@link ConfigurationState#isCodeCompletionsEnabled()}. - * - * @see EnableCompletionsAction - * @see DisableCompletionsAction - */ -public interface CodeCompletionEnabledListener extends EventListener { - - /** - * Topic for subscribing to {@link ConfigurationState#isCodeCompletionsEnabled()} changes.
    - * Broadcasts from Application-Level to all projects. - */ - @Topic.AppLevel - Topic TOPIC = new Topic<>(CodeCompletionEnabledListener.class, - BroadcastDirection.TO_DIRECT_CHILDREN); - - void onCodeCompletionsEnabledChange(boolean codeCompletionsEnabled); -} - diff --git a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java deleted file mode 100644 index 1b08fa0a..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java +++ /dev/null @@ -1,41 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; -import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import java.util.List; -import org.jetbrains.annotations.NotNull; - -/** - * Disables code-completion.
    Publishes message to {@link CodeCompletionEnabledListener#TOPIC} - */ -public class DisableCompletionsAction extends AnAction { - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - ConfigurationSettings.getCurrentState().setCodeCompletionsEnabled(false); - ApplicationManager.getApplication() - .getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC) - .onCodeCompletionsEnabledChange(false); - } - - @Override - public void update(@NotNull AnActionEvent e) { - var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - var codeCompletionEnabled = ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled(); - e.getPresentation().setEnabled(codeCompletionEnabled); - e.getPresentation() - .setVisible(codeCompletionEnabled && List.of(OPENAI, LLAMA_CPP).contains(selectedService)); - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java deleted file mode 100644 index e646e997..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java +++ /dev/null @@ -1,41 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; -import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import java.util.List; -import org.jetbrains.annotations.NotNull; - -/** - * Enables code-completion.
    Publishes message to {@link CodeCompletionEnabledListener#TOPIC} - */ -public class EnableCompletionsAction extends AnAction { - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - ConfigurationSettings.getCurrentState().setCodeCompletionsEnabled(true); - ApplicationManager.getApplication() - .getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC) - .onCodeCompletionsEnabledChange(true); - } - - @Override - public void update(@NotNull AnActionEvent e) { - var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - var codeCompletionEnabled = ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled(); - e.getPresentation().setEnabled(!codeCompletionEnabled); - e.getPresentation() - .setVisible(!codeCompletionEnabled && List.of(OPENAI, LLAMA_CPP).contains(selectedService)); - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index cbcf7fa9..bfe6c0c7 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -161,8 +161,6 @@ public class ConfigurationComponent { state.setCreateNewChatOnEachAction(openNewTabCheckBox.isSelected()); state.setMethodNameGenerationEnabled(methodNameGenerationCheckBox.isSelected()); state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected()); - state.setCodeCompletionsEnabled( - ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled()); return state; } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index f11663d3..dbf9bb6e 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -20,7 +20,6 @@ public class ConfigurationState { private boolean methodNameGenerationEnabled = true; private boolean captureCompileErrors = true; private boolean autoFormattingEnabled = true; - private boolean codeCompletionsEnabled; private Map tableData = EditorActionsUtil.DEFAULT_ACTIONS; public String getSystemPrompt() { @@ -119,14 +118,6 @@ public class ConfigurationState { this.autoFormattingEnabled = autoFormattingEnabled; } - public boolean isCodeCompletionsEnabled() { - return codeCompletionsEnabled; - } - - public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { - this.codeCompletionsEnabled = codeCompletionsEnabled; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -144,7 +135,6 @@ public class ConfigurationState { && methodNameGenerationEnabled == that.methodNameGenerationEnabled && captureCompileErrors == that.captureCompileErrors && autoFormattingEnabled == that.autoFormattingEnabled - && codeCompletionsEnabled == that.codeCompletionsEnabled && Objects.equals(systemPrompt, that.systemPrompt) && Objects.equals(commitMessagePrompt, that.commitMessagePrompt) && Objects.equals(tableData, that.tableData); @@ -154,7 +144,6 @@ public class ConfigurationState { public int hashCode() { return Objects.hash(systemPrompt, commitMessagePrompt, maxTokens, temperature, checkForPluginUpdates, createNewChatOnEachAction, ignoreGitCommitTokenLimit, - methodNameGenerationEnabled, captureCompileErrors, autoFormattingEnabled, - codeCompletionsEnabled, tableData); + methodNameGenerationEnabled, captureCompileErrors, autoFormattingEnabled, tableData); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java index c0c6de0b..cb81c722 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java @@ -27,6 +27,8 @@ public class LlamaSettingsState { private double topP = 0.9; private double minP = 0.05; private double repeatPenalty = 1.1; + private boolean codeCompletionsEnabled = true; + private int codeCompletionMaxTokens = 128; public boolean isUseCustomModel() { return useCustomModel; @@ -168,6 +170,22 @@ public class LlamaSettingsState { this.repeatPenalty = repeatPenalty; } + public boolean isCodeCompletionsEnabled() { + return codeCompletionsEnabled; + } + + public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { + this.codeCompletionsEnabled = codeCompletionsEnabled; + } + + public int getCodeCompletionMaxTokens() { + return codeCompletionMaxTokens; + } + + public void setCodeCompletionMaxTokens(int codeCompletionMaxTokens) { + this.codeCompletionMaxTokens = codeCompletionMaxTokens; + } + private static Integer getRandomAvailablePortOrDefault() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); @@ -201,7 +219,9 @@ public class LlamaSettingsState { && remoteModelInfillPromptTemplate == that.remoteModelInfillPromptTemplate && Objects.equals(baseHost, that.baseHost) && Objects.equals(serverPort, that.serverPort) - && Objects.equals(additionalParameters, that.additionalParameters); + && Objects.equals(additionalParameters, that.additionalParameters) + && codeCompletionsEnabled == that.codeCompletionsEnabled + && codeCompletionMaxTokens == that.codeCompletionMaxTokens; } @Override @@ -209,6 +229,7 @@ public class LlamaSettingsState { return Objects.hash(runLocalServer, useCustomModel, customLlamaModelPath, huggingFaceModel, localModelPromptTemplate, remoteModelPromptTemplate, localModelInfillPromptTemplate, remoteModelInfillPromptTemplate, baseHost, serverPort, contextSize, threads, - additionalParameters, topK, topP, minP, repeatPenalty); + additionalParameters, topK, topP, minP, repeatPenalty, codeCompletionsEnabled, + codeCompletionMaxTokens); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java index caa9de03..7147eceb 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java @@ -5,6 +5,7 @@ import static ee.carlrobert.codegpt.ui.UIUtil.withEmptyLeftBorder; import com.intellij.ui.TitledSeparator; import com.intellij.util.ui.FormBuilder; import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState; import java.awt.BorderLayout; @@ -14,10 +15,14 @@ public class LlamaSettingsForm extends JPanel { private final LlamaServerPreferencesForm llamaServerPreferencesForm; private final LlamaRequestPreferencesForm llamaRequestPreferencesForm; + private final CodeCompletionConfigurationForm codeCompletionConfigurationForm; public LlamaSettingsForm(LlamaSettingsState settings) { llamaServerPreferencesForm = new LlamaServerPreferencesForm(settings); llamaRequestPreferencesForm = new LlamaRequestPreferencesForm(settings); + codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( + settings.isCodeCompletionsEnabled(), + settings.getCodeCompletionMaxTokens()); init(); } @@ -44,6 +49,8 @@ public class LlamaSettingsForm extends JPanel { state.setLocalModelPromptTemplate(modelPreferencesForm.getPromptTemplate()); state.setLocalModelInfillPromptTemplate(modelPreferencesForm.getInfillPromptTemplate()); + state.setCodeCompletionsEnabled(codeCompletionConfigurationForm.isCodeCompletionsEnabled()); + state.setCodeCompletionMaxTokens(codeCompletionConfigurationForm.getMaxTokens()); return state; } @@ -51,6 +58,8 @@ public class LlamaSettingsForm extends JPanel { var state = LlamaSettings.getCurrentState(); llamaServerPreferencesForm.resetForm(state); llamaRequestPreferencesForm.resetForm(state); + codeCompletionConfigurationForm.setCodeCompletionsEnabled(state.isCodeCompletionsEnabled()); + codeCompletionConfigurationForm.setMaxTokens(state.getCodeCompletionMaxTokens()); } public LlamaServerPreferencesForm getLlamaServerPreferencesForm() { @@ -60,6 +69,8 @@ public class LlamaSettingsForm extends JPanel { private void init() { setLayout(new BorderLayout()); add(FormBuilder.createFormBuilder() + .addComponent(new TitledSeparator("Code Completions")) + .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponent(new TitledSeparator( CodeGPTBundle.get("settingsConfigurable.service.llama.serverPreferences.title"))) .addComponent(llamaServerPreferencesForm.getForm()) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java index d65a1576..4ba05dcd 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java @@ -7,6 +7,7 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; 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 fe3d22b6..f0bdc78b 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 @@ -12,6 +12,7 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import javax.swing.JPanel; @@ -22,6 +23,7 @@ public class OpenAISettingsForm { private final JBPasswordField apiKeyField; private final JBTextField organizationField; private final ComboBox completionModelComboBox; + private final CodeCompletionConfigurationForm codeCompletionConfigurationForm; public OpenAISettingsForm(OpenAISettingsState settings) { apiKeyField = new JBPasswordField(); @@ -32,6 +34,9 @@ public class OpenAISettingsForm { new EnumComboBoxModel<>(OpenAIChatCompletionModel.class)); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(settings.getModel())); + codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( + settings.isCodeCompletionsEnabled(), + settings.getCodeCompletionMaxTokens()); } public JPanel getForm() { @@ -52,6 +57,8 @@ public class OpenAISettingsForm { .createPanel(); return FormBuilder.createFormBuilder() + .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.codeCompletions"))) + .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.configuration"))) .addComponent(withEmptyLeftBorder(configurationGrid)) .addComponentFillVertically(new JPanel(), 0) @@ -73,6 +80,8 @@ public class OpenAISettingsForm { var state = new OpenAISettingsState(); state.setModel(getModel()); state.setOrganization(organizationField.getText()); + state.setCodeCompletionsEnabled(codeCompletionConfigurationForm.isCodeCompletionsEnabled()); + state.setCodeCompletionMaxTokens(codeCompletionConfigurationForm.getMaxTokens()); return state; } @@ -82,5 +91,7 @@ public class OpenAISettingsForm { completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(state.getModel())); organizationField.setText(state.getOrganization()); + codeCompletionConfigurationForm.setCodeCompletionsEnabled(state.isCodeCompletionsEnabled()); + codeCompletionConfigurationForm.setMaxTokens(state.getCodeCompletionMaxTokens()); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java index 75134d65..b13df7d1 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java @@ -7,6 +7,8 @@ public class OpenAISettingsState { private String organization = ""; private String model = OpenAIChatCompletionModel.GPT_3_5_0125_16k.getCode(); + private boolean codeCompletionsEnabled = true; + private int codeCompletionMaxTokens = 128; public String getOrganization() { return organization; @@ -24,6 +26,22 @@ public class OpenAISettingsState { this.model = model; } + public boolean isCodeCompletionsEnabled() { + return codeCompletionsEnabled; + } + + public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { + this.codeCompletionsEnabled = codeCompletionsEnabled; + } + + public int getCodeCompletionMaxTokens() { + return codeCompletionMaxTokens; + } + + public void setCodeCompletionMaxTokens(int codeCompletionMaxTokens) { + this.codeCompletionMaxTokens = codeCompletionMaxTokens; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -33,11 +51,14 @@ public class OpenAISettingsState { return false; } OpenAISettingsState that = (OpenAISettingsState) o; - return Objects.equals(organization, that.organization) && Objects.equals(model, that.model); + return Objects.equals(organization, that.organization) + && Objects.equals(model, that.model) + && codeCompletionsEnabled == that.codeCompletionsEnabled + && codeCompletionMaxTokens == that.codeCompletionMaxTokens; } @Override public int hashCode() { - return Objects.hash(organization, model); + return Objects.hash(organization, model, codeCompletionsEnabled, codeCompletionMaxTokens); } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt new file mode 100644 index 00000000..c9dc223e --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -0,0 +1,54 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP +import ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings + +abstract class CodeCompletionFeatureToggleActions( + private val enableFeatureAction: Boolean +) : DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + GeneralSettings.getCurrentState().selectedService + .takeIf { it in listOf(OPENAI, LLAMA_CPP) } + ?.also { selectedService -> + if (OPENAI == selectedService) { + OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } else { + LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } + } + } + + override fun update(e: AnActionEvent) { + val selectedService = GeneralSettings.getCurrentState().selectedService + val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) + e.presentation.isEnabled = codeCompletionEnabled != enableFeatureAction + e.presentation.isVisible = + e.presentation.isEnabled && listOf(OPENAI, LLAMA_CPP).contains( + selectedService + ) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } + + private fun isCodeCompletionsEnabled(serviceType: ServiceType): Boolean { + return when (serviceType) { + OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled + else -> false + } + } +} + +class EnableCompletionsAction : CodeCompletionFeatureToggleActions(true) + +class DisableCompletionsAction : CodeCompletionFeatureToggleActions(false) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index 0738062c..26c35063 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -2,34 +2,34 @@ package ee.carlrobert.codegpt.codecompletions import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest object CodeCompletionRequestFactory { - private const val MAX_TOKENS = 128 - fun buildOpenAIRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) .setSuffix(details.suffix) .setStream(true) - .setMaxTokens(MAX_TOKENS) + .setMaxTokens(OpenAISettings.getCurrentState().codeCompletionMaxTokens) .setTemperature(0.4) .build() } fun buildLlamaRequest(details: InfillRequestDetails): LlamaCompletionRequest { - val promptTemplate = getLlamaInfillPromptTemplate() + val settings = LlamaSettings.getCurrentState() + val promptTemplate = getLlamaInfillPromptTemplate(settings) val prompt = promptTemplate.buildPrompt(details.prefix, details.suffix) return LlamaCompletionRequest.Builder(prompt) - .setN_predict(MAX_TOKENS) + .setN_predict(settings.codeCompletionMaxTokens) .setStream(true) .setTemperature(0.4) .setStop(promptTemplate.stopTokens) .build() } - private fun getLlamaInfillPromptTemplate(): InfillPromptTemplate { - val settings = LlamaSettings.getCurrentState() + private fun getLlamaInfillPromptTemplate(settings: LlamaSettingsState): InfillPromptTemplate { if (!settings.isRunLocalServer) { return settings.remoteModelInfillPromptTemplate } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index 6d111e23..e0fecdce 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -8,7 +8,10 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.completions.CompletionRequestService -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.codegpt.treesitter.CodeCompletionParserFactory import ee.carlrobert.llm.completion.CompletionEventListener import kotlinx.coroutines.* @@ -52,8 +55,13 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { } override fun isEnabled(event: InlineCompletionEvent): Boolean { - return event is InlineCompletionEvent.DocumentChange - && ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled + val selectedService = GeneralSettings.getCurrentState().selectedService + val codeCompletionsEnabled = when (selectedService) { + ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled + else -> false + } + return event is InlineCompletionEvent.DocumentChange && codeCompletionsEnabled } private fun ProducerScope.getCodeCompletionEventListener( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt new file mode 100644 index 00000000..1fddb3fa --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt @@ -0,0 +1,49 @@ +package ee.carlrobert.codegpt.settings.service + +import com.intellij.openapi.ui.panel.ComponentPanelBuilder +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.fields.IntegerField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import javax.swing.JPanel + +class CodeCompletionConfigurationForm(codeCompletionsEnabled: Boolean, maxTokens: Int) { + + private val codeCompletionsEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), + codeCompletionsEnabled + ) + private val codeCompletionMaxTokensField = + IntegerField("completion_max_tokens", 8, 4096).apply { + columns = 12 + value = maxTokens + } + + fun getForm(): JPanel { + return FormBuilder.createFormBuilder() + .addComponent(codeCompletionsEnabledCheckBox) + .addVerticalGap(4) + .addLabeledComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensLabel"), + codeCompletionMaxTokensField + ) + .addComponentToRightColumn( + ComponentPanelBuilder.createCommentComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensComment"), true, 48, true + ) + ) + .panel + } + + var isCodeCompletionsEnabled: Boolean + get() = codeCompletionsEnabledCheckBox.isSelected + set(enabled) { + codeCompletionsEnabledCheckBox.isSelected = enabled + } + + var maxTokens: Int + get() = codeCompletionMaxTokensField.value + set(maxTokens) { + codeCompletionMaxTokensField.value = maxTokens + } +} diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index b5858202..c31b2b00 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -198,3 +198,7 @@ action.attachImage=Attach Image action.attachImageDescription=Attach an image imageFileChooser.title=Select Image imageAccordion.title=Attached image +shared.codeCompletions=Code Completions +codeCompletionsForm.enableFeatureText=Enable code completions +codeCompletionsForm.maxTokensLabel=Max tokens: +codeCompletionsForm.maxTokensComment=The maximum number of tokens that can be generated in the code completion. diff --git a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java index 0b039e06..bdd390a4 100644 --- a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java +++ b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java @@ -8,8 +8,7 @@ import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse; import static org.assertj.core.api.Assertions.assertThat; import com.intellij.openapi.editor.VisualPosition; -import com.intellij.testFramework.PlatformTestUtil; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange; import java.util.List; import testsupport.IntegrationTest; @@ -20,7 +19,7 @@ public class CodeCompletionServiceTest extends IntegrationTest { public void testFetchCodeCompletionLlama() { useLlamaService(); - ConfigurationSettings.getCurrentState().setCodeCompletionsEnabled(true); + LlamaSettings.getCurrentState().setCodeCompletionsEnabled(true); myFixture.configureByText( "CompletionTest.java", getResourceContent("/codecompletions/code-completion-file.txt")); From fef8f6f903ff2a719264f4c8a848b0e221269a8a Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 3 Apr 2024 02:07:54 +0300 Subject: [PATCH 06/20] docs: update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c930b4..1f6198a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- A couple of IntelliJ Platform errors/warnings +- Fixed several UI/UX issues related to code completions for IDE versions starting from 233 - Error when adding a single file to the context +- A couple of IntelliJ Platform errors/warnings ### Removed From 3246102f0699e2d9b6b16d1e5f238072f4ed3302 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 3 Apr 2024 16:50:41 +0300 Subject: [PATCH 07/20] docs: update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6198a5..25f0ec18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,17 +11,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models - Total token panel for all providers - Support for configuring code completions via settings +- Support for You Pro modes +- Basic post-processing for code completions +- Code completion feature toggle keyboard-shortcut +- Support for git commit message generation with Custom OpenAI and Anthropic services ### Fixed - Fixed several UI/UX issues related to code completions for IDE versions starting from 233 - Error when adding a single file to the context - A couple of IntelliJ Platform errors/warnings +- Several IntelliJ platform warnings ### Removed - Azure custom configuration (use OpenAI-compatible service to override the default configuration) +### Changed + +- Supported minimum IDE build from 213 to 222 + ## [2.5.1] - 2024-03-14 ### Added From 314fbfbafcf38e8760afcb9c9ffdb6b572c12d94 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Thu, 4 Apr 2024 16:16:59 +0300 Subject: [PATCH 08/20] docs: update plugin description --- DESCRIPTION.md | 102 ++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 05ef6fdb..23b201ef 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -2,104 +2,68 @@ ## Introducing CodeGPT: Your Free, Open-Source AI Copilot for Coding -CodeGPT is your go-to AI assistant, designed to enhance your coding skills and optimize your programming time. -Access state-of-the-art LLMs like GPT-4, Claude 3, Code LLama and more, all for free. +CodeGPT is your go-to AI coding assistant, offering assistance throughout your entire software development journey while keeping privacy in mind. Access state-of-the-art large language models from leading providers such as OpenAI, Anthropic, Azure, Mistral, and others, or connect to a locally hosted model for a completely offline and transparent development experience. -## Quick Start Guide +## Core Features -1. **Download the Plugin** +Leveraging large language models, CodeGPT offers a wide range of features to enhance your coding experience, including, but not limited to: -2. **Choose Your Preferred Service** +### Code Completions - a) **OpenAI** - Requires authentication via OpenAI API key. +Receive single-line or whole-function autocomplete suggestions as you type. - b) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, Groq, Ollama and many more. - - c) **Anthropic** - Requires authentication via API key. +![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) - d) **Azure** - Requires authentication via Active Directory or API key. +> Completions are currently supported only for OpenAI GPT-3.5 and locally hosted models. - e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. +### Chat (with Vision) - f) **LLaMA C/C++ Port** - Run Code Llama, WizardCoder, Deepseek Coder, and other state-of-the-art models locally for free. +Get instant coding advice through a ChatGPT-like interface. Ask questions, seek explanations, or get guidance on your projects without leaving your IDE. -3. **Start Using the Features** +CodeGPT also supports vision models and image understanding, allowing you to attach images for more context-aware assistance. It can detect new screenshots automatically, saving you time by eliminating the need to manually upload images each time you take a screenshot. -### OpenAI +![Chat with Vision](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/chat-interface.png?raw=true) -After successful installation, configure your API key. Navigate to the plugin's settings via **File | Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. +### Commit Message Generation -### Azure OpenAI +CodeGPT can generate meaningful commit messages based on the changes made in your codebase. It analyzes the diff of your staged changes and suggests concise and descriptive commit messages, saving you time and effort. -For Azure OpenAI services, you'll need to input three additional fields: +![Commit Message Generation](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/generate-commit-message.png?raw=true) -- **Resource name**: The name of your Azure OpenAI Cognitive Services. -- **Deployment ID**: The name of your Deployment. -- **API version**: The most recent non-preview version. +### Reference Files -Also, input one of the two provided API keys. +CodeGPT allows you to reference specific files or documentation during your chat sessions, ensuring that responses are always relevant and accurate. -### You.com (Free) +![Reference Files](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/reference-files-modal.png?raw=true) -**You**.com is a search engine that summarizes the best parts of the internet for **you**, with private ads and with privacy options. +### Name Suggestions -**You⚡Pro** +Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, helping you adhere to best practices and maintain readability in your codebase. -Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. +![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) -Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more details. +### Offline Development Support -### LLaMA C/C++ Port (Free, Local) +CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. -> **Note**: This feature is currently supported only on Linux and MacOS. +![Offline Development Support](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/llama-settings.png?raw=true) -The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. +## Privacy -#### Getting Started +**Your data stays yours.** CodeGPT **does not** collect or store any kind of sensitive information. -1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. +However, with users' consent, we do collect anonymous usage data, which we use to understand how users interact with the extension, including the most-used features and preferred providers. -2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` button. A status message will be displayed, indicating that the server is starting up. +## License -3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on the `Apply/OK` button to save your settings and start using the application. +CodeGPT's code is open source under the Apache License 2.0. -animated +## Feedback -> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the port and click `Apply/OK`. +Your input helps us grow. Reach out through: -## Features - -The plugin provides several key features, such as: - -### Chat with AI - -Ask anything you'd like. - -animated - -### Select and Ask - -Ask anything related to your selected code. - -animated - -### Replace Generated Code - -Instantly replace a selected code block in the editor with suggested code generated by AI. - -animated - -### Regenerate Response - -Expected a different answer? Re-generate any response of your choosing. - -animated - -## Other features - -- **Conversation History** - View recent conversation history and restore previous sessions, making it easy to pick up where you left off -- **Concurrent conversations** - Chat with the AI in multiple tabs simultaneously -- **Seamless conversations** - Chat with the AI regardless of the maximum token limitations -- **Predefined Actions** - Create your own editor actions or override the existing ones, saving time rewriting the same prompt repeatedly +- [Issue Tracker](https://github.com/carlrobertoh/CodeGPT/issues) +- [Discord](https://discord.gg/8dTGGrwcnR) +- [Email](mailto:carlrobertoh@gmail.com) From 17cfbf43de78ea39086b9594883a8ae9ef220403 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Thu, 4 Apr 2024 16:38:14 +0300 Subject: [PATCH 09/20] docs: update readme --- README.md | 75 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index eddfa98e..c39d590b 100644 --- a/README.md +++ b/README.md @@ -38,49 +38,51 @@ ## About The Project -This is an extension for JetBrains IDEs that integrates AI into your coding environment. -By leveraging the power of Large Language Models (LLMs), this makes it an invaluable tool for developers looking to streamline their workflow and gain a deeper understanding of the code they're working on. +CodeGPT is your go-to AI coding assistant, offering assistance throughout your entire software development journey while keeping privacy in mind. Access state-of-the-art large language models from leading providers such as OpenAI, Anthropic, Azure, Mistral, and others, or connect to a locally hosted model for a completely offline and transparent development experience. -## Features +## Core Features -The plugin provides several key features, such as: +Leveraging large language models, CodeGPT offers a wide range of features to enhance your coding experience, including, but not limited to: -### Chat with AI +### Code Completions -

    - animated -

    +Receive single-line or whole-function autocomplete suggestions as you type. -### Chat With Multiple Files +![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) -

    - animated -

    +> **Note**: Currently supported only on GPT-3.5 and locally-hosted models. -### Choose Between Different Providers +### Chat (with Vision) -

    - -

    +Get instant coding advice through a ChatGPT-like interface. Ask questions, seek explanations, or get guidance on your projects without leaving your IDE. -### Method Name Suggestions +CodeGPT also supports vision models and image understanding, allowing you to attach images for more context-aware assistance. It can detect new screenshots automatically, saving you time by eliminating the need to manually upload images each time you take a screenshot. -

    - -

    +![Chat with Vision](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/chat-interface.png?raw=true) -### Generate Commit Messages +### Commit Message Generation -

    - -

    +CodeGPT can generate meaningful commit messages based on the changes made in your codebase. It analyzes the diff of your staged changes and suggests concise and descriptive commit messages, saving you time and effort. -### Other features +![Commit Message Generation](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/generate-commit-message.png?raw=true) -- **Conversation History** - View recent conversation history and restore previous sessions, making it easy to pick up where you left off -- **Concurrent conversations** - Chat with AI in multiple tabs simultaneously -- **Seamless conversations** - Chat with AI regardless of the maximum token limitations -- **Predefined Prompts** - Create your own editor prompt or override the existing ones +### Reference Files + +CodeGPT allows you to reference specific files or documentation during your chat sessions, ensuring that responses are always relevant and accurate. + +![Reference Files](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/reference-files-modal.png?raw=true) + +### Name Suggestions + +Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, helping you adhere to best practices and maintain readability in your codebase. + +![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) + +### Offline Development Support + +CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. + +![Offline Development Support](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/llama-settings.png?raw=true) ## Getting Started @@ -173,9 +175,19 @@ git submodule update tail -f build/idea-sandbox/system/log/idea.log ``` -## Issues +## Privacy -See the [open issues][open-issues] for a full list of proposed features (and known issues). +**Your data stays yours.** CodeGPT **does not** collect or store any kind of sensitive information. + +However, with users' consent, we do collect anonymous usage data, which we use to understand how users interact with the extension, including the most-used features and preferred providers. + +## Feedback + +Your input helps us grow. Reach out through: + +- [Issue Tracker](https://github.com/carlrobertoh/CodeGPT/issues) +- [Discord](https://discord.gg/8dTGGrwcnR) +- [Email](mailto:carlrobertoh@gmail.com) ## License @@ -184,7 +196,6 @@ Apache 2.0 © [Carl-Robert Linnupuu][portfolio] If you found this project interesting, kindly rate it on the marketplace and don't forget to give it a star. Thanks again!

    (back to top)

    - From 9ed95f4e4eb6ba654944d4db2458fd2259d8efa0 Mon Sep 17 00:00:00 2001 From: Artem Borzov Date: Fri, 5 Apr 2024 21:02:18 +0500 Subject: [PATCH 10/20] fix: correctly handle changed files to generate a commit message #338 (#433) * fix: properly handle changed files to generate commit message (resolve #338) * fix: re-include staged diff in the final prompt --------- Co-authored-by: borzov Co-authored-by: Carl-Robert Linnupuu --- .../GenerateGitCommitMessageAction.java | 121 ++++++++++-------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index 0e5b07bf..9553a886 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -4,7 +4,6 @@ import static com.intellij.openapi.ui.Messages.OK; import static com.intellij.util.ObjectUtils.tryCast; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; @@ -20,11 +19,8 @@ import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsDataKeys; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ui.ChangesBrowserBase; -import com.intellij.openapi.vcs.changes.ui.CommitDialogChangesBrowser; import com.intellij.openapi.vcs.ui.CommitMessage; -import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.vcs.commit.CommitWorkflowUi; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; @@ -37,11 +33,13 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; import java.util.stream.Stream; import okhttp3.sse.EventSource; import org.jetbrains.annotations.NotNull; @@ -61,19 +59,17 @@ public class GenerateGitCommitMessageAction extends AnAction { @Override public void update(@NotNull AnActionEvent event) { + var commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - if (selectedService == YOU) { + if (selectedService == YOU || commitWorkflowUi == null) { event.getPresentation().setVisible(false); return; } - var includedChangesFilePaths = getIncludedChangesFilePaths(event); - var includedUnversionedChangesFilePaths = getIncludedUnversionedFilePaths(event); - var filesSelected = - !includedChangesFilePaths.isEmpty() || !includedUnversionedChangesFilePaths.isEmpty(); var callAllowed = CompletionRequestService.isRequestAllowed( GeneralSettings.getCurrentState().getSelectedService()); - event.getPresentation().setEnabled(callAllowed && filesSelected); + event.getPresentation().setEnabled(callAllowed + && new CommitWorkflowChanges(commitWorkflowUi).isFilesSelected()); event.getPresentation().setText(CodeGPTBundle.get(callAllowed ? "action.generateCommitMessage.title" : "action.generateCommitMessage.missingCredentials")); @@ -86,11 +82,7 @@ public class GenerateGitCommitMessageAction extends AnAction { return; } - var gitDiff = getGitDiff( - project, - getIncludedChangesFilePaths(event), - getIncludedUnversionedFilePaths(event)); - + var gitDiff = getGitDiff(event, project); var tokenCount = encodingManager.countTokens(gitDiff); if (tokenCount > MAX_TOKEN_COUNT_WARNING && OverlayUtil.showTokenSoftLimitWarningDialog(tokenCount) != OK) { @@ -142,21 +134,48 @@ public class GenerateGitCommitMessageAction extends AnAction { return commitMessage != null ? commitMessage.getEditorField().getEditor() : null; } - private String getGitDiff( - Project project, - List includedChangesFilePaths, - List includedUnversionedFilePaths) { + private String getGitDiff(AnActionEvent event, Project project) { + var commitWorkflowUi = Optional.ofNullable(event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI)) + .orElseThrow(() -> new IllegalStateException("Could not retrieve commit workflow ui.")); + var changes = new CommitWorkflowChanges(commitWorkflowUi); + var projectBasePath = project.getBasePath(); + var gitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), false); + var stagedGitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), true); + var newFilesContent = + getNewFilesDiff(projectBasePath, changes.getIncludedUnversionedFilePaths()); + return Stream.of( - new AbstractMap.SimpleEntry<>(includedChangesFilePaths, true), - new AbstractMap.SimpleEntry<>(includedUnversionedFilePaths, false)) - .filter(entry -> !entry.getKey().isEmpty()) - .map(entry -> { - var process = - createGitDiffProcess(project.getBasePath(), entry.getKey(), entry.getValue()); - return new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines() - .collect(joining("\n")); + new AbstractMap.SimpleEntry<>("Git diff", gitDiff), + new AbstractMap.SimpleEntry<>("Staged git diff", stagedGitDiff), + new AbstractMap.SimpleEntry<>("New files", newFilesContent)) + .filter(entry -> !entry.getValue().isEmpty()) + .map(entry -> "%s:\n%s".formatted(entry.getKey(), entry.getValue())) + .collect(joining("\n\n")); + } + + private String getGitDiff(String projectPath, List filePaths, boolean cached) { + if (filePaths.isEmpty()) { + return ""; + } + + var process = createGitDiffProcess(projectPath, filePaths, cached); + return new BufferedReader(new InputStreamReader(process.getInputStream())) + .lines() + .collect(joining("\n")); + } + + private String getNewFilesDiff(String projectPath, List filePaths) { + return filePaths.stream() + .map(pathString -> { + var filePath = Path.of(pathString); + var relativePath = Path.of(projectPath).relativize(filePath); + try { + return "New file '" + relativePath + "' content:\n" + Files.readString(filePath); + } catch (IOException ignored) { + return null; + } }) + .filter(Objects::nonNull) .collect(joining("\n")); } @@ -178,29 +197,31 @@ public class GenerateGitCommitMessageAction extends AnAction { } } - private @NotNull List getFilePaths( - AnActionEvent event, - Function> extractor) { - var changesBrowserBase = event.getData(ChangesBrowserBase.DATA_KEY); - if (changesBrowserBase == null) { - return List.of(); + static class CommitWorkflowChanges { + + private final List includedVersionedFilePaths; + private final List includedUnversionedFilePaths; + + CommitWorkflowChanges(CommitWorkflowUi commitWorkflowUi) { + includedVersionedFilePaths = commitWorkflowUi.getIncludedChanges().stream() + .map(it -> it.getVirtualFile() == null ? null : it.getVirtualFile().getPath()) + .filter(Objects::nonNull) + .toList(); + includedUnversionedFilePaths = commitWorkflowUi.getIncludedUnversionedFiles().stream() + .map(FilePath::getPath) + .toList(); } - return extractor.apply((CommitDialogChangesBrowser) changesBrowserBase) - .map(obj -> obj instanceof Change - ? ((Change) obj).getVirtualFile() - : ((FilePath) obj).getVirtualFile()) - .filter(Objects::nonNull) - .map(VirtualFile::getPath) - .distinct() - .collect(toList()); - } + public List getIncludedVersionedFilePaths() { + return includedVersionedFilePaths; + } - private @NotNull List getIncludedChangesFilePaths(AnActionEvent event) { - return getFilePaths(event, browser -> browser.getIncludedChanges().stream()); - } + public List getIncludedUnversionedFilePaths() { + return includedUnversionedFilePaths; + } - private @NotNull List getIncludedUnversionedFilePaths(AnActionEvent event) { - return getFilePaths(event, browser -> browser.getIncludedUnversionedFiles().stream()); + public boolean isFilesSelected() { + return !includedVersionedFilePaths.isEmpty() || !includedUnversionedFilePaths.isEmpty(); + } } } From 2048f87fa8fbc963490d5cadbb870d3be417bb96 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Fri, 5 Apr 2024 19:05:14 +0300 Subject: [PATCH 11/20] docs: update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f0ec18..ae3c4f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Git commit message generation - Fixed several UI/UX issues related to code completions for IDE versions starting from 233 - Error when adding a single file to the context -- A couple of IntelliJ Platform errors/warnings - Several IntelliJ platform warnings ### Removed From 30025d23785f41681d704a4fa5c63676627bca75 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sat, 6 Apr 2024 00:47:53 +0300 Subject: [PATCH 12/20] feat: request focus for text area on toolwindow state changes (closes #423) --- .../toolwindow/ChatToolWindowListener.kt | 24 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt new file mode 100644 index 00000000..04de3b71 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt @@ -0,0 +1,24 @@ +package ee.carlrobert.codegpt.toolwindow; + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.ex.ToolWindowManagerListener +import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager + +class ChatToolWindowListener : ToolWindowManagerListener { + + override fun stateChanged(toolWindowManager: ToolWindowManager) { + toolWindowManager.getToolWindow("CodeGPT")?.run { + if (isVisible) requestFocusForTextArea(project) + } + } + + private fun requestFocusForTextArea(project: Project) { + val contentManager = project.getService(StandardChatToolWindowContentManager::class.java) + contentManager.tryFindChatTabbedPane().ifPresent { tabbedPane -> + tabbedPane.tryFindActiveTabPanel().ifPresent { tabPanel -> + tabPanel.requestFocusForTextArea() + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d8539467..b2cb1b3a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,6 +9,8 @@ + From 71aee5f6aae2410259e3fde20e213259a9300eaf Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sat, 6 Apr 2024 02:02:12 +0300 Subject: [PATCH 13/20] fix: prevent sending completion to a closed channel --- .../CodeGPTInlineCompletionProvider.kt | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index e0fecdce..a1a42b5f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -1,11 +1,9 @@ package ee.carlrobert.codegpt.codecompletions import com.intellij.codeInsight.inline.completion.* -import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.completions.CompletionRequestService import ee.carlrobert.codegpt.settings.GeneralSettings @@ -14,10 +12,11 @@ import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.codegpt.treesitter.CodeCompletionParserFactory import ee.carlrobert.llm.completion.CompletionEventListener -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.sse.EventSource import java.util.concurrent.atomic.AtomicReference @@ -28,7 +27,6 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { } private val currentCall = AtomicReference(null) - private val providerScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) override val id: InlineCompletionProviderID get() = InlineCompletionProviderID("CodeGPTInlineCompletionProvider") @@ -43,11 +41,19 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { val infillRequest = withContext(Dispatchers.EDT) { InfillRequestDetails.fromInlineCompletionRequest(request) } - cancelCurrentCall() currentCall.set( CompletionRequestService.getInstance().getCodeCompletionAsync( infillRequest, - getCodeCompletionEventListener(request.editor, infillRequest) + CodeCompletionEventListener(infillRequest) { + request.editor.putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, it) + launch { + try { + trySend(InlineCompletionGrayTextElement(it)) + } catch (e: Exception) { + LOG.error("Failed to send inline completion suggestion", e) + } + } + } ) ) awaitClose { cancelCurrentCall() } @@ -64,20 +70,6 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { return event is InlineCompletionEvent.DocumentChange && codeCompletionsEnabled } - private fun ProducerScope.getCodeCompletionEventListener( - editor: Editor, - infillRequest: InfillRequestDetails - ) = CodeCompletionEventListener(infillRequest) { - editor.putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, it) - providerScope.launch { - try { - send(InlineCompletionGrayTextElement(it)) - } catch (e: Exception) { - LOG.error("Failed to send inline completion suggestion", e) - } - } - } - private fun cancelCurrentCall() { currentCall.getAndSet(null)?.cancel() } From efe0f0b74adf68598761ac7df1752453efcd5692 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sat, 6 Apr 2024 02:24:32 +0300 Subject: [PATCH 14/20] fix: use the proper callback for text area autofocus --- .../codegpt/toolwindow/ChatToolWindowListener.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt index 04de3b71..73981d27 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt @@ -1,15 +1,15 @@ package ee.carlrobert.codegpt.toolwindow; import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ex.ToolWindowManagerListener import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager class ChatToolWindowListener : ToolWindowManagerListener { - override fun stateChanged(toolWindowManager: ToolWindowManager) { - toolWindowManager.getToolWindow("CodeGPT")?.run { - if (isVisible) requestFocusForTextArea(project) + override fun toolWindowShown(toolWindow: ToolWindow) { + if ("CodeGPT" == toolWindow.id) { + requestFocusForTextArea(toolWindow.project) } } From 52ceaa6a2686601eb87590188c8bad559549f93a Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sat, 6 Apr 2024 02:25:05 +0300 Subject: [PATCH 15/20] docs: update changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3c4f7c..7aaf18c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models - Total token panel for all providers - Support for configuring code completions via settings -- Support for You Pro modes -- Basic post-processing for code completions -- Code completion feature toggle keyboard-shortcut -- Support for git commit message generation with Custom OpenAI and Anthropic services +- Autofocus for UserTextArea when the tool window is visible ### Fixed From 7f505e2c30b73b65becc3b75669b701b796cd940 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:41:02 +0200 Subject: [PATCH 16/20] chore(deps): Update and centralize dependencies (#436) * chore(deps): Update and centralize dependencies * Update treesitter to 0.22.2 * Update kotlin to 1.9.23 * Update jackson to 2.17.0 * Update gradle-intellij-plugin to 1.17.3 * Update gradle to 8.7 * Use BOMs where possible * Centralize dependencies in version catalog * Allow Dependabot to update other modules (add treesitter and buildSrc/src/main/kotlin, remove core) * fix: preload credentials only once for all headers --- .github/dependabot.yml | 4 +-- build.gradle.kts | 17 +++++----- buildSrc/build.gradle.kts | 4 +-- buildSrc/settings.gradle.kts | 7 ++++ .../codegpt.java-conventions.gradle.kts | 9 +++--- codegpt-telemetry/build.gradle.kts | 4 +-- codegpt-treesitter/build.gradle.kts | 4 +-- gradle.properties | 4 +-- gradle/libs.versions.toml | 30 ++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 14 ++++---- .../CompletionRequestProvider.java | 4 +-- 13 files changed, 71 insertions(+), 32 deletions(-) create mode 100644 buildSrc/settings.gradle.kts create mode 100644 gradle/libs.versions.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95500085..89d07c80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,11 +11,11 @@ updates: interval: "daily" - package-ecosystem: "gradle" - directory: "/buildSrc" + directory: "/buildSrc/src/main/kotlin" # /buildSrc and /codegpt-telemetry use only references schedule: interval: "daily" - package-ecosystem: "gradle" - directory: "/codegpt-core" + directory: "/codegpt-treesitter" schedule: interval: "daily" diff --git a/build.gradle.kts b/build.gradle.kts index f6c9f1d1..5ce2eaf3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ fun environment(key: String) = providers.environmentVariable(key) plugins { id("codegpt.java-conventions") - id("org.jetbrains.changelog") version "2.2.0" + alias(libs.plugins.changelog) } group = properties("pluginGroup").get() @@ -50,15 +50,16 @@ dependencies { implementation(project(":codegpt-telemetry")) implementation(project(":codegpt-treesitter")) - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.2") - implementation("com.vladsch.flexmark:flexmark-all:0.64.8") { + implementation(enforcedPlatform("com.fasterxml.jackson:jackson-bom:${libs.versions.jackson.get()}")) + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation(libs.flexmark.all) { // vulnerable transitive dependency exclude(group = "org.jsoup", module = "jsoup") } - implementation("org.jsoup:jsoup:1.17.2") - implementation("org.apache.commons:commons-text:1.11.0") - implementation("com.knuddels:jtokkit:1.0.0") + implementation(libs.jsoup) + implementation(libs.commons.text) + implementation(libs.jtokkit) } tasks.register("updateSubmodules") { @@ -154,4 +155,4 @@ tasks { showStandardStreams = true } } -} \ No newline at end of file +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a7ce63c8..006b09c9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,6 @@ repositories { } dependencies { - implementation("org.jetbrains.intellij.plugins", "gradle-intellij-plugin", "1.17.2") - implementation("org.jetbrains.kotlin", "kotlin-gradle-plugin", "1.9.22") + implementation(libs.gradle.intellij.plugin) + implementation(libs.kotlin.gradle.plugin) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..85123139 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) // Allow references + } + } +} diff --git a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts index 767f856a..f42cd97f 100644 --- a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts @@ -28,11 +28,12 @@ checkstyle { dependencies { implementation("ee.carlrobert:llm-client:0.7.0") + testImplementation(enforcedPlatform("org.junit:junit-bom:5.10.2")) testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") - testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } tasks { diff --git a/codegpt-telemetry/build.gradle.kts b/codegpt-telemetry/build.gradle.kts index ca2c2a3a..60546d9e 100644 --- a/codegpt-telemetry/build.gradle.kts +++ b/codegpt-telemetry/build.gradle.kts @@ -3,5 +3,5 @@ plugins { } dependencies { - implementation("com.rudderstack.sdk.java.analytics:analytics:3.0.0") -} \ No newline at end of file + implementation(libs.analytics) +} diff --git a/codegpt-treesitter/build.gradle.kts b/codegpt-treesitter/build.gradle.kts index ed515253..532f50f5 100644 --- a/codegpt-treesitter/build.gradle.kts +++ b/codegpt-treesitter/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementation("io.github.bonede:tree-sitter:0.21.0") + implementation(libs.tree.sitter) implementation("io.github.bonede:tree-sitter-erlang:0.1.0") implementation("io.github.bonede:tree-sitter-elixir:0.1.1") implementation("io.github.bonede:tree-sitter-dockerfile:0.1.2") @@ -37,4 +37,4 @@ dependencies { implementation("io.github.bonede:tree-sitter-php:0.20.0") implementation("io.github.bonede:tree-sitter-typescript:0.20.3") implementation("io.github.bonede:tree-sitter-query:0.1.0") -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 8e067c49..aed14e03 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ platformPlugins = javaVersion = 17 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 8.5 +gradleVersion = 8.7 # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency = false @@ -38,4 +38,4 @@ org.gradle.caching = true systemProp.org.gradle.unsafe.kotlin.assignment = true # Temporary workaround for Kotlin Compiler OutOfMemoryError -> https://jb.gg/intellij-platform-kotlin-oom -kotlin.incremental.useClasspathSnapshot = false \ No newline at end of file +kotlin.incremental.useClasspathSnapshot = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..8d2e3bde --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,30 @@ +[versions] +analytics = "3.0.0" +assertj = "3.25.3" +changelog = "2.2.0" +commons-text = "1.11.0" +flexmark = "0.64.8" +gradle-intellij-plugin-version="1.17.3" +jackson = "2.17.0" +jsoup = "1.17.2" +jtokkit = "1.0.0" +junit = "5.10.2" +kotlin = "1.9.23" +llm-client = "0.7.0" +tree-sitter = "0.22.2" + +[libraries] +analytics = { module = "com.rudderstack.sdk.java.analytics:analytics", version.ref = "analytics" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } +flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" } +gradle-intellij-plugin = { module = "org.jetbrains.intellij.plugins:gradle-intellij-plugin", version.ref = "gradle-intellij-plugin-version" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +jtokkit = { module = "com.knuddels:jtokkit", version.ref = "jtokkit" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +llm-client = { module = "ee.carlrobert:llm-client", version.ref = "llm-client" } +tree-sitter = { module = "io.github.bonede:tree-sitter", version.ref = "tree-sitter" } + +[plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

    iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..b82aa23a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 328cd04b..35976cf1 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -216,10 +216,10 @@ public class CompletionRequestProvider { List messages, boolean streamRequest) { var requestBuilder = new Request.Builder().url(customConfiguration.getUrl().trim()); + var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); for (var entry : customConfiguration.getHeaders().entrySet()) { String value = entry.getValue(); - var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); - if (value.contains("$CUSTOM_SERVICE_API_KEY") && credential != null) { + if (credential != null && value.contains("$CUSTOM_SERVICE_API_KEY")) { value = value.replace("$CUSTOM_SERVICE_API_KEY", credential); } requestBuilder.addHeader(entry.getKey(), value); From 4688a1c8d04a2aa3c23738a15a52c599c79b4048 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 7 Apr 2024 15:35:15 +0300 Subject: [PATCH 17/20] refactor: remove 'Standard' prefix from toolwindow component class names, and other minor cleanup --- .../ProjectCompilationStatusListener.java | 4 +- .../codegpt/actions/editor/AskAction.java | 4 +- .../actions/editor/CustomPromptAction.java | 4 +- .../actions/editor/EditorActionsUtil.java | 4 +- .../DeleteAllConversationsAction.java | 4 +- .../settings/GeneralSettingsConfigurable.java | 4 +- .../toolwindow/ProjectToolWindowFactory.java | 4 +- ...java => ChatToolWindowContentManager.java} | 25 ++-- ...dowPanel.java => ChatToolWindowPanel.java} | 22 ++-- .../chat/ChatToolWindowTabPanel.java | 109 +++++++++++++----- ...ane.java => ChatToolWindowTabbedPane.java} | 20 ++-- .../chat/standard/EditorAction.java | 46 -------- .../chat/standard/EditorActionEvent.java | 9 -- .../StandardChatToolWindowLandingPanel.java | 70 ----------- .../StandardChatToolWindowTabPanel.java | 84 -------------- .../conversations/ConversationPanel.java | 6 +- .../{ => chat}/ChatToolWindowListener.kt | 5 +- .../ui/ChatToolWindowLandingPanel.kt | 100 ++++++++++++++++ src/main/resources/META-INF/plugin.xml | 2 +- ...t.java => ChatToolWindowTabPanelTest.java} | 13 +-- ...java => ChatToolWindowTabbedPaneTest.java} | 16 ++- 21 files changed, 249 insertions(+), 306 deletions(-) rename src/main/java/ee/carlrobert/codegpt/toolwindow/chat/{standard/StandardChatToolWindowContentManager.java => ChatToolWindowContentManager.java} (81%) rename src/main/java/ee/carlrobert/codegpt/toolwindow/chat/{standard/StandardChatToolWindowPanel.java => ChatToolWindowPanel.java} (89%) rename src/main/java/ee/carlrobert/codegpt/toolwindow/chat/{standard/StandardChatToolWindowTabbedPane.java => ChatToolWindowTabbedPane.java} (88%) delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java rename src/main/kotlin/ee/carlrobert/codegpt/toolwindow/{ => chat}/ChatToolWindowListener.kt (74%) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt rename src/test/java/ee/carlrobert/codegpt/toolwindow/chat/{StandardChatToolWindowTabPanelTest.java => ChatToolWindowTabPanelTest.java} (96%) rename src/test/java/ee/carlrobert/codegpt/toolwindow/chat/{StandardChatToolWindowTabbedPaneTest.java => ChatToolWindowTabbedPaneTest.java} (64%) diff --git a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java index b1b86157..1563d497 100644 --- a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java +++ b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java @@ -16,7 +16,7 @@ import com.intellij.openapi.project.Project; import ee.carlrobert.codegpt.completions.CompletionRequestProvider; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.OverlayUtil; import java.io.File; import java.util.ArrayList; @@ -50,7 +50,7 @@ public class ProjectCompilationStatusListener implements CompilationStatusListen NotificationType.INFORMATION) .addAction(NotificationAction.createSimpleExpiring( CodeGPTBundle.get("notification.compilationError.okLabel"), - () -> project.getService(StandardChatToolWindowContentManager.class) + () -> project.getService(ChatToolWindowContentManager.class) .sendMessage(getMultiFileMessage(compileContext), FIX_COMPILE_ERRORS))) .addAction(NotificationAction.createSimpleExpiring( CodeGPTBundle.get("shared.notification.doNotShowAgain"), diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java index f0d5836d..b83f4b88 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java @@ -5,7 +5,7 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.ConversationsState; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import org.jetbrains.annotations.NotNull; public class AskAction extends AnAction { @@ -26,7 +26,7 @@ public class AskAction extends AnAction { if (project != null) { ConversationsState.getInstance().setCurrentConversation(null); var tabPanel = - project.getService(StandardChatToolWindowContentManager.class).createNewTabPanel(); + project.getService(ChatToolWindowContentManager.class).createNewTabPanel(); if (tabPanel != null) { tabPanel.displayLandingView(); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java index f870f0b3..05db8729 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java @@ -11,7 +11,7 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.event.ActionEvent; @@ -42,7 +42,7 @@ public class CustomPromptAction extends BaseEditorAction { format("%s\n```%s\n%s\n```", previousUserPrompt, fileExtension, selectedText)); message.setUserMessage(previousUserPrompt); SwingUtilities.invokeLater(() -> - project.getService(StandardChatToolWindowContentManager.class).sendMessage(message)); + project.getService(ChatToolWindowContentManager.class).sendMessage(message)); } } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java index 06d02345..5c242df7 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -14,7 +14,7 @@ import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.util.file.FileUtil; import java.util.Collection; import java.util.LinkedHashMap; @@ -65,7 +65,7 @@ public class EditorActionsUtil { format("\n```%s\n%s\n```", fileExtension, selectedText))); message.setUserMessage(prompt.replace("{{selectedCode}}", "")); var toolWindowContentManager = - project.getService(StandardChatToolWindowContentManager.class); + project.getService(ChatToolWindowContentManager.class); toolWindowContentManager.getToolWindow().show(); message.setReferencedFilePaths( diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java index d9f7d71f..6490853a 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java @@ -11,7 +11,7 @@ import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import org.jetbrains.annotations.NotNull; public class DeleteAllConversationsAction extends AnAction { @@ -44,7 +44,7 @@ public class DeleteAllConversationsAction extends AnAction { if (project != null) { try { ConversationService.getInstance().clearAll(); - project.getService(StandardChatToolWindowContentManager.class).resetAll(); + project.getService(ChatToolWindowContentManager.class).resetAll(); } finally { TelemetryAction.IDE_ACTION.createActionMessage() .property("action", ActionType.DELETE_ALL_CONVERSATIONS.name()) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index 11063a53..b9d781bc 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -26,7 +26,7 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.util.ApplicationUtil; import javax.swing.JComponent; import org.jetbrains.annotations.Nls; @@ -160,6 +160,6 @@ public class GeneralSettingsConfigurable implements Configurable { throw new RuntimeException("Could not find current project."); } - project.getService(StandardChatToolWindowContentManager.class).resetAll(); + project.getService(ChatToolWindowContentManager.class).resetAll(); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java index e3c43b2c..655cdc68 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java @@ -6,7 +6,7 @@ import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; import com.intellij.ui.content.ContentManagerEvent; import com.intellij.ui.content.ContentManagerListener; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel; import ee.carlrobert.codegpt.toolwindow.conversations.ConversationsToolWindow; import javax.swing.JComponent; import org.jetbrains.annotations.NotNull; @@ -14,7 +14,7 @@ import org.jetbrains.annotations.NotNull; public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware { public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { - var chatToolWindowPanel = new StandardChatToolWindowPanel(project, toolWindow.getDisposable()); + var chatToolWindowPanel = new ChatToolWindowPanel(project, toolWindow.getDisposable()); var conversationsToolWindow = new ConversationsToolWindow(project); addContent(toolWindow, chatToolWindowPanel, "Chat"); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java similarity index 81% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java index 27ea30bd..b66814c9 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat; import static java.util.Objects.requireNonNull; @@ -22,11 +22,11 @@ import java.util.Optional; import org.jetbrains.annotations.NotNull; @Service(Service.Level.PROJECT) -public final class StandardChatToolWindowContentManager { +public final class ChatToolWindowContentManager { private final Project project; - public StandardChatToolWindowContentManager(Project project) { + public ChatToolWindowContentManager(Project project) { this.project = project; } @@ -55,15 +55,14 @@ public final class StandardChatToolWindowContentManager { .ifPresent(tabbedPane -> tabbedPane.tryFindTabTitle(conversation.getId()) .ifPresentOrElse( title -> tabbedPane.setSelectedIndex(tabbedPane.indexOfTab(title)), - () -> tabbedPane.addNewTab( - new StandardChatToolWindowTabPanel(project, conversation)))); + () -> tabbedPane.addNewTab(new ChatToolWindowTabPanel(project, conversation)))); } - public StandardChatToolWindowTabPanel createNewTabPanel() { + public ChatToolWindowTabPanel createNewTabPanel() { displayChatTab(); return tryFindChatTabbedPane() .map(item -> { - var panel = new StandardChatToolWindowTabPanel( + var panel = new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation()); item.addNewTab(panel); @@ -83,26 +82,26 @@ public final class StandardChatToolWindowContentManager { ); } - public Optional tryFindChatTabbedPane() { + public Optional tryFindChatTabbedPane() { var chatTabContent = tryFindFirstChatTabContent(); if (chatTabContent.isPresent()) { - var chatToolWindowPanel = (StandardChatToolWindowPanel) chatTabContent.get().getComponent(); + var chatToolWindowPanel = (ChatToolWindowPanel) chatTabContent.get().getComponent(); return Optional.of(chatToolWindowPanel.getChatTabbedPane()); } return Optional.empty(); } - public Optional tryFindChatToolWindowPanel() { + public Optional tryFindChatToolWindowPanel() { return tryFindFirstChatTabContent() .map(ComponentContainer::getComponent) - .filter(component -> component instanceof StandardChatToolWindowPanel) - .map(component -> (StandardChatToolWindowPanel) component); + .filter(component -> component instanceof ChatToolWindowPanel) + .map(component -> (ChatToolWindowPanel) component); } public void resetAll() { tryFindChatTabbedPane().ifPresent(tabbedPane -> { tabbedPane.clearAll(); - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel( + tabbedPane.addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation())); }); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java similarity index 89% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java index 178c024e..ea40de64 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat; import static java.lang.String.format; import static java.util.Collections.emptyList; @@ -30,13 +30,13 @@ import javax.swing.BoxLayout; import javax.swing.JPanel; import org.jetbrains.annotations.NotNull; -public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { +public class ChatToolWindowPanel extends SimpleToolWindowPanel { private final ToolWindowFooterNotification selectedFilesNotification; private final ToolWindowFooterNotification imageFileAttachmentNotification; - private StandardChatToolWindowTabbedPane tabbedPane; + private ChatToolWindowTabbedPane tabbedPane; - public StandardChatToolWindowPanel( + public ChatToolWindowPanel( @NotNull Project project, @NotNull Disposable parentDisposable) { super(true); @@ -58,7 +58,7 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { "File path: " + filePath)); } - public StandardChatToolWindowTabbedPane getChatTabbedPane() { + public ChatToolWindowTabbedPane getChatTabbedPane() { return tabbedPane; } @@ -96,10 +96,10 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { conversation = ConversationService.getInstance().startConversation(); } - var tabPanel = new StandardChatToolWindowTabPanel(project, conversation); + var tabPanel = new ChatToolWindowTabPanel(project, conversation); tabbedPane = createTabbedPane(tabPanel, parentDisposable); Runnable onAddNewTab = () -> { - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel( + tabbedPane.addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation())); repaint(); @@ -122,7 +122,7 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { private ActionToolbar createActionToolbar( Project project, - StandardChatToolWindowTabbedPane tabbedPane, + ChatToolWindowTabbedPane tabbedPane, Runnable onAddNewTab) { var actionGroup = new DefaultCompactActionGroup("TOOLBAR_ACTION_GROUP", false); actionGroup.add(new CreateNewConversationAction(onAddNewTab)); @@ -137,10 +137,10 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { return toolbar; } - private StandardChatToolWindowTabbedPane createTabbedPane( - StandardChatToolWindowTabPanel tabPanel, + private ChatToolWindowTabbedPane createTabbedPane( + ChatToolWindowTabPanel tabPanel, Disposable parentDisposable) { - var tabbedPane = new StandardChatToolWindowTabbedPane(parentDisposable); + var tabbedPane = new ChatToolWindowTabbedPane(parentDisposable); tabbedPane.addNewTab(tabPanel); return tabbedPane; } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 3a9194b9..95cf70a8 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -26,8 +26,8 @@ import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; @@ -36,6 +36,8 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea; +import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.BorderLayout; @@ -48,23 +50,21 @@ import java.util.UUID; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; +import kotlin.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public abstract class ChatToolWindowTabPanel implements Disposable { +public class ChatToolWindowTabPanel implements Disposable { private static final Logger LOG = Logger.getInstance(ChatToolWindowTabPanel.class); + private final Project project; private final JPanel rootPanel; private final Conversation conversation; private final UserPromptTextArea userPromptTextArea; private final ConversationService conversationService; - - protected final Project project; - protected final TotalTokensPanel totalTokensPanel; - protected final ChatToolWindowScrollablePanel toolWindowScrollablePanel; - - protected abstract JComponent getLandingView(); + private final TotalTokensPanel totalTokensPanel; + private final ChatToolWindowScrollablePanel toolWindowScrollablePanel; public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) { this.project = project; @@ -80,6 +80,12 @@ public abstract class ChatToolWindowTabPanel implements Disposable { rootPanel = createRootPanel(); userPromptTextArea.requestFocusInWindow(); userPromptTextArea.requestFocus(); + + if (conversation.getMessages().isEmpty()) { + displayLandingView(); + } else { + displayConversation(conversation); + } } public void dispose() { @@ -94,6 +100,19 @@ public abstract class ChatToolWindowTabPanel implements Disposable { return conversation; } + public TotalTokensDetails getTokenDetails() { + return totalTokensPanel.getTokenDetails(); + } + + public void requestFocusForTextArea() { + userPromptTextArea.focus(); + } + + public void displayLandingView() { + toolWindowScrollablePanel.displayLandingView(getLandingView()); + totalTokensPanel.updateConversationTokens(conversation); + } + public void sendMessage(Message message) { sendMessage(message, ConversationType.DEFAULT); } @@ -101,7 +120,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { public void sendMessage(Message message, ConversationType conversationType) { SwingUtilities.invokeLater(() -> { var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); - var chatToolWindowPanel = project.getService(StandardChatToolWindowContentManager.class) + var chatToolWindowPanel = project.getService(ChatToolWindowContentManager.class) .tryFindChatToolWindowPanel(); if (referencedFiles != null && !referencedFiles.isEmpty()) { var referencedFilePaths = referencedFiles.stream() @@ -164,20 +183,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { .addContent(new ChatMessageResponseBody(project, true, this)); } - public TotalTokensDetails getTokenDetails() { - return totalTokensPanel.getTokenDetails(); - } - - public void requestFocusForTextArea() { - userPromptTextArea.focus(); - } - - public void displayLandingView() { - toolWindowScrollablePanel.displayLandingView(getLandingView()); - totalTokensPanel.updateConversationTokens(conversation); - } - - protected void reloadMessage( + private void reloadMessage( Message message, Conversation conversation, ConversationType conversationType) { @@ -205,7 +211,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { } } - protected void removeMessage(UUID messageId, Conversation conversation) { + private void removeMessage(UUID messageId, Conversation conversation) { toolWindowScrollablePanel.removeMessage(messageId); conversation.removeMessage(messageId); conversationService.saveConversation(conversation); @@ -216,7 +222,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { } } - protected void clearWindow() { + private void clearWindow() { toolWindowScrollablePanel.clearAll(); totalTokensPanel.updateConversationTokens(conversation); } @@ -268,7 +274,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { panel.setBorder(JBUI.Borders.compound( JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), JBUI.Borders.empty(8))); - var contentManager = project.getService(StandardChatToolWindowContentManager.class); + var contentManager = project.getService(ChatToolWindowContentManager.class); panel.add(JBUI.Panels.simplePanel(createUserPromptTextAreaHeader( selectedService, () -> { @@ -290,6 +296,57 @@ public abstract class ChatToolWindowTabPanel implements Disposable { .createCustomComponent(ActionPlaces.UNKNOWN)); } + private JComponent getLandingView() { + return new ChatToolWindowLandingPanel((action, locationOnScreen) -> { + var editor = EditorUtil.getSelectedEditor(project); + if (editor == null || !editor.getSelectionModel().hasSelection()) { + OverlayUtil.showWarningBalloon( + editor == null ? "Unable to locate a selected editor" + : "Please select a target code before proceeding", + locationOnScreen); + return Unit.INSTANCE; + } + + var fileExtension = FileUtil.getFileExtension( + ((EditorImpl) editor).getVirtualFile().getName()); + var message = new Message(action.getPrompt().replace( + "{{selectedCode}}", + format("\n```%s\n%s\n```", fileExtension, editor.getSelectionModel().getSelectedText()))); + message.setUserMessage(action.getUserMessage()); + + sendMessage(message, ConversationType.DEFAULT); + return Unit.INSTANCE; + }); + } + + private void displayConversation(@NotNull Conversation conversation) { + clearWindow(); + conversation.getMessages().forEach(message -> { + var messageResponseBody = + new ChatMessageResponseBody(project, this).withResponse(message.getResponse()); + + var serpResults = message.getSerpResults(); + if (YouSettings.getCurrentState().isDisplayWebSearchResults() + && serpResults != null && !serpResults.isEmpty()) { + messageResponseBody.displaySerpResults(serpResults); + } + messageResponseBody.hideCaret(); + + var userMessagePanel = new UserMessagePanel(project, message, this); + var imageFilePath = message.getImageFilePath(); + if (imageFilePath != null && !imageFilePath.isEmpty()) { + userMessagePanel.displayImage(imageFilePath); + } + + var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); + messagePanel.add(userMessagePanel); + messagePanel.add(new ResponsePanel() + .withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT)) + .withDeleteAction(() -> removeMessage(message.getId(), conversation)) + .addContent(messageResponseBody)); + }); + } + private JPanel createRootPanel() { var gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java similarity index 88% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java index d623b032..ca7551f5 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat; import com.intellij.icons.AllIcons; import com.intellij.openapi.Disposable; @@ -25,9 +25,9 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; -public class StandardChatToolWindowTabbedPane extends JBTabbedPane { +public class ChatToolWindowTabbedPane extends JBTabbedPane { - private final Map activeTabMapping = new TreeMap<>( + private final Map activeTabMapping = new TreeMap<>( (o1, o2) -> { int n1 = Integer.parseInt(o1.replaceAll("\\D", "")); int n2 = Integer.parseInt(o2.replaceAll("\\D", "")); @@ -35,18 +35,18 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { }); private final Disposable parentDisposable; - public StandardChatToolWindowTabbedPane(Disposable parentDisposable) { + public ChatToolWindowTabbedPane(Disposable parentDisposable) { this.parentDisposable = parentDisposable; setTabComponentInsets(null); setComponentPopupMenu(new TabPopupMenu()); addChangeListener(e -> refreshTabState()); } - public Map getActiveTabMapping() { + public Map getActiveTabMapping() { return activeTabMapping; } - public void addNewTab(StandardChatToolWindowTabPanel toolWindowPanel) { + public void addNewTab(ChatToolWindowTabPanel toolWindowPanel) { var tabIndices = activeTabMapping.keySet().toArray(new String[0]); var nextIndex = 0; for (String title : tabIndices) { @@ -81,7 +81,7 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { .map(Map.Entry::getKey); } - public Optional tryFindActiveTabPanel() { + public Optional tryFindActiveTabPanel() { var selectedIndex = getSelectedIndex(); if (selectedIndex == -1) { return Optional.empty(); @@ -116,7 +116,7 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { Disposer.dispose(tabPanel); activeTabMapping.remove(getTitleAt(getSelectedIndex())); removeTabAt(getSelectedIndex()); - addNewTab(new StandardChatToolWindowTabPanel( + addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation())); repaint(); @@ -180,8 +180,8 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { @Override public void show(Component invoker, int x, int y) { - selectedPopupTabIndex = StandardChatToolWindowTabbedPane.this.getUI() - .tabForCoordinate(StandardChatToolWindowTabbedPane.this, x, y); + selectedPopupTabIndex = ChatToolWindowTabbedPane.this.getUI() + .tabForCoordinate(ChatToolWindowTabbedPane.this, x, y); if (selectedPopupTabIndex > 0) { super.show(invoker, x, y); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java deleted file mode 100644 index 5baf396d..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java +++ /dev/null @@ -1,46 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -enum EditorAction { - FIND_BUGS( - "Find Bugs", - "Find bugs in the selected code", - "Find bugs and output code with bugs fixed in the selected code: {{selectedCode}}"), - WRITE_TESTS( - "Write Tests", - "Write unit tests for the selected code", - "Write unit tests for the selected code: {{selectedCode}}"), - EXPLAIN( - "Explain", - "Explain the selected code", - "Explain the selected code: {{selectedCode}}"), - REFACTOR( - "Refactor", - "Refactor the selected code", - "Refactor the selected code: {{selectedCode}}"), - OPTIMIZE( - "Optimize", - "Optimize the selected code", - "Optimize the selected code: {{selectedCode}}"); - - private final String label; - private final String userMessage; - private final String prompt; - - EditorAction(String label, String userMessage, String prompt) { - this.label = label; - this.userMessage = userMessage; - this.prompt = prompt; - } - - public String getLabel() { - return label; - } - - public String getPrompt() { - return prompt; - } - - public String getUserMessage() { - return userMessage; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java deleted file mode 100644 index c8af738c..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import java.awt.Point; - -@FunctionalInterface -public interface EditorActionEvent { - - void handleAction(EditorAction action, Point locationOnScreen); -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java deleted file mode 100644 index eab81f22..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java +++ /dev/null @@ -1,70 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import static java.lang.String.format; - -import com.intellij.ui.components.ActionLink; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.Icons; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; -import ee.carlrobert.codegpt.ui.UIUtil; -import java.awt.BorderLayout; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JPanel; - -class StandardChatToolWindowLandingPanel extends ResponsePanel { - - StandardChatToolWindowLandingPanel(EditorActionEvent onAction) { - addContent(createContent(onAction)); - } - - private ActionLink createEditorActionLink(EditorAction action, EditorActionEvent onAction) { - var link = new ActionLink(action.getUserMessage(), event -> { - onAction.handleAction(action, ((ActionLink) event.getSource()).getLocationOnScreen()); - }); - link.setIcon(Icons.Sparkle); - return link; - } - - private JPanel createContent(EditorActionEvent onAction) { - var panel = new JPanel(new BorderLayout()); - panel.add(UIUtil.createTextPane( - "" - + format( - "

    " - + "Welcome %s, I'm your intelligent code companion, here to be" - + " your partner-in-crime for getting things done in a flash." - + "

    ", GeneralSettings.getCurrentState().getDisplayName()) - + "

    " - + "Feel free to ask me anything you'd like, but my true superpower lies in assisting " - + "you with your code! Here are a few examples of how I can assist you:" - + "

    " - + "", - false), BorderLayout.NORTH); - panel.add(createEditorActionsListPanel(onAction), BorderLayout.CENTER); - panel.add(UIUtil.createTextPane( - "" - + "

    " - + "Being an AI-powered assistant, I may occasionally have surprises or make mistakes. " - + "Therefore, it's wise to double-check any code or suggestions I provide." - + "

    " - + "", - false), BorderLayout.SOUTH); - return panel; - } - - private JPanel createEditorActionsListPanel(EditorActionEvent onAction) { - var listPanel = new JPanel(); - listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.PAGE_AXIS)); - listPanel.setBorder(JBUI.Borders.emptyLeft(4)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.WRITE_TESTS, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.EXPLAIN, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.FIND_BUGS, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - return listPanel; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java deleted file mode 100644 index 5b8645cf..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java +++ /dev/null @@ -1,84 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import static java.lang.String.format; - -import com.intellij.openapi.editor.impl.EditorImpl; -import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.completions.ConversationType; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.settings.service.you.YouSettings; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; -import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; -import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.codegpt.util.file.FileUtil; -import javax.swing.JComponent; -import org.jetbrains.annotations.NotNull; - -public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel { - - public StandardChatToolWindowTabPanel( - @NotNull Project project, - @NotNull Conversation conversation) { - super(project, conversation); - if (conversation.getMessages().isEmpty()) { - displayLandingView(); - } else { - displayConversation(conversation); - } - } - - @Override - protected JComponent getLandingView() { - return new StandardChatToolWindowLandingPanel((action, locationOnScreen) -> { - var editor = EditorUtil.getSelectedEditor(project); - if (editor == null || !editor.getSelectionModel().hasSelection()) { - OverlayUtil.showWarningBalloon( - editor == null ? "Unable to locate a selected editor" - : "Please select a target code before proceeding", - locationOnScreen); - return; - } - - var fileExtension = FileUtil.getFileExtension( - ((EditorImpl) editor).getVirtualFile().getName()); - var message = new Message(action.getPrompt().replace( - "{{selectedCode}}", - format("\n```%s\n%s\n```", fileExtension, editor.getSelectionModel().getSelectedText()))); - message.setUserMessage(action.getUserMessage()); - - sendMessage(message, ConversationType.DEFAULT); - }); - } - - private void displayConversation(@NotNull Conversation conversation) { - clearWindow(); - conversation.getMessages().forEach(message -> { - var messageResponseBody = - new ChatMessageResponseBody(project, this).withResponse(message.getResponse()); - - var serpResults = message.getSerpResults(); - if (YouSettings.getCurrentState().isDisplayWebSearchResults() - && serpResults != null && !serpResults.isEmpty()) { - messageResponseBody.displaySerpResults(serpResults); - } - messageResponseBody.hideCaret(); - - var userMessagePanel = new UserMessagePanel(project, message, this); - var imageFilePath = message.getImageFilePath(); - if (imageFilePath != null && !imageFilePath.isEmpty()) { - userMessagePanel.displayImage(imageFilePath); - } - - var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(userMessagePanel); - messagePanel.add(new ResponsePanel() - .withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT)) - .withDeleteAction(() -> removeMessage(message.getId(), conversation)) - .addContent(messageResponseBody)); - }); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java index 4e82f767..d3386062 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java @@ -9,7 +9,7 @@ import ee.carlrobert.codegpt.actions.toolwindow.DeleteConversationAction; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.IconActionButton; import ee.carlrobert.codegpt.ui.ModelIconLabel; import java.awt.BorderLayout; @@ -30,12 +30,12 @@ class ConversationPanel extends JPanel { @NotNull Conversation conversation, @NotNull Runnable onDelete) { super(new BorderLayout()); - var toolWindowContentManager = project.getService(StandardChatToolWindowContentManager.class); + var toolWindowContentManager = project.getService(ChatToolWindowContentManager.class); init(toolWindowContentManager, conversation, onDelete); } private void init( - StandardChatToolWindowContentManager toolWindowContentManager, + ChatToolWindowContentManager toolWindowContentManager, Conversation conversation, Runnable onDelete) { setBackground(JBColor.background()); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt similarity index 74% rename from src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt rename to src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt index 73981d27..6c15f218 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ChatToolWindowListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt @@ -1,9 +1,8 @@ -package ee.carlrobert.codegpt.toolwindow; +package ee.carlrobert.codegpt.toolwindow.chat import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ex.ToolWindowManagerListener -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager class ChatToolWindowListener : ToolWindowManagerListener { @@ -14,7 +13,7 @@ class ChatToolWindowListener : ToolWindowManagerListener { } private fun requestFocusForTextArea(project: Project) { - val contentManager = project.getService(StandardChatToolWindowContentManager::class.java) + val contentManager = project.getService(ChatToolWindowContentManager::class.java) contentManager.tryFindChatTabbedPane().ifPresent { tabbedPane -> tabbedPane.tryFindActiveTabPanel().ifPresent { tabPanel -> tabPanel.requestFocusForTextArea() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt new file mode 100644 index 00000000..7318ea3b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt @@ -0,0 +1,100 @@ +package ee.carlrobert.codegpt.toolwindow.ui + +import com.intellij.ui.components.ActionLink +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel +import ee.carlrobert.codegpt.ui.UIUtil.createTextPane +import java.awt.BorderLayout +import java.awt.Point +import java.awt.event.ActionListener +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.JPanel + +class ChatToolWindowLandingPanel(onAction: (LandingPanelAction, Point) -> Unit) : ResponsePanel() { + + init { + addContent(createContent(onAction)) + } + + private fun createContent(onAction: (LandingPanelAction, Point) -> Unit): JPanel { + return JPanel(BorderLayout()).apply { + add(createTextPane(getWelcomeMessage(), false), BorderLayout.NORTH) + add(createActionsListPanel(onAction), BorderLayout.CENTER) + add(createTextPane(getCautionMessage(), false), BorderLayout.SOUTH) + } + } + + private fun createActionsListPanel(onAction: (LandingPanelAction, Point) -> Unit): JPanel { + val listPanel = JPanel() + listPanel.layout = BoxLayout(listPanel, BoxLayout.PAGE_AXIS) + listPanel.border = JBUI.Borders.emptyLeft(4) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.WRITE_TESTS, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.EXPLAIN, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.FIND_BUGS, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + return listPanel + } + + private fun createEditorActionLink( + action: LandingPanelAction, + onAction: (LandingPanelAction, Point) -> Unit + ): ActionLink { + return ActionLink(action.userMessage, ActionListener { event -> + onAction(action, (event.source as ActionLink).locationOnScreen) + }).apply { + icon = Icons.Sparkle + } + } + + private fun getWelcomeMessage(): String { + return """ + +

    + Welcome ${GeneralSettings.getCurrentState().displayName}, I'm your intelligent code companion, here to be your partner-in-crime for getting things done in a flash. +

    +

    + Feel free to ask me anything you'd like, but my true superpower lies in assisting you with your code! Here are a few examples of how I can assist you: +

    + + """.trimIndent() + } + + private fun getCautionMessage(): String { + return """ + +

    + Being an AI-powered assistant, I may occasionally have surprises or make mistakes. Therefore, it's wise to double-check any code or suggestions I provide. +

    + + """.trimIndent() + } +} + +enum class LandingPanelAction( + val label: String, + val userMessage: String, + val prompt: String +) { + FIND_BUGS( + "Find Bugs", + "Find bugs in the selected code", + "Find bugs and output code with bugs fixed in the selected code: {{selectedCode}}" + ), + WRITE_TESTS( + "Write Tests", + "Write unit tests for the selected code", + "Write unit tests for the selected code: {{selectedCode}}" + ), + EXPLAIN( + "Explain", + "Explain the selected code", + "Explain the selected code: {{selectedCode}}" + ) +} + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b2cb1b3a..554d81b9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,7 +9,7 @@ - diff --git a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java similarity index 96% rename from src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java rename to src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java index 3aba409d..4835894b 100644 --- a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java +++ b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java @@ -20,7 +20,6 @@ import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel; import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange; import java.io.IOException; import java.nio.file.Files; @@ -30,14 +29,14 @@ import java.util.List; import java.util.Map; import testsupport.IntegrationTest; -public class StandardChatToolWindowTabPanelTest extends IntegrationTest { +public class ChatToolWindowTabPanelTest extends IntegrationTest { public void testSendingOpenAIMessage() { useOpenAIService(); ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT); var message = new Message("Hello!"); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -102,7 +101,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { message.setReferencedFilePaths( List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -188,7 +187,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT); var message = new Message("TEST_MESSAGE"); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -266,7 +265,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { message.setReferencedFilePaths( List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -360,7 +359,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { llamaSettings.setRepeatPenalty(1.3); var message = new Message("TEST_PROMPT"); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectLlama((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/completion"); assertThat(request.getBody()) diff --git a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java similarity index 64% rename from src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java rename to src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java index 17411989..0746f538 100644 --- a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java +++ b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java @@ -6,13 +6,11 @@ import com.intellij.openapi.util.Disposer; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabbedPane; -public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { +public class ChatToolWindowTabbedPaneTest extends BasePlatformTestCase { public void testClearAllTabs() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); tabbedPane.addNewTab(createNewTabPanel()); tabbedPane.clearAll(); @@ -22,7 +20,7 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { public void testAddingNewTabs() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); tabbedPane.addNewTab(createNewTabPanel()); tabbedPane.addNewTab(createNewTabPanel()); @@ -33,10 +31,10 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { } public void testResetCurrentlyActiveTabPanel() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); var conversation = ConversationService.getInstance().startConversation(); conversation.addMessage(new Message("TEST_PROMPT", "TEST_RESPONSE")); - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel(getProject(), conversation)); + tabbedPane.addNewTab(new ChatToolWindowTabPanel(getProject(), conversation)); tabbedPane.resetCurrentlyActiveTabPanel(getProject()); @@ -44,8 +42,8 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { assertThat(tabPanel.getConversation().getMessages()).isEmpty(); } - private StandardChatToolWindowTabPanel createNewTabPanel() { - return new StandardChatToolWindowTabPanel( + private ChatToolWindowTabPanel createNewTabPanel() { + return new ChatToolWindowTabPanel( getProject(), ConversationService.getInstance().startConversation()); } From 00c9813eeb74de65008fba91e5942336fd69f694 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:53:59 +0200 Subject: [PATCH 18/20] chore(deps): Update checkstyle to 10.15.0 (#438) * Cleaned versions catalog and dependencies * Fixed checkstyle finding --- build.gradle.kts | 6 +++++- .../src/main/kotlin/codegpt.java-conventions.gradle.kts | 2 +- gradle/libs.versions.toml | 7 +++---- .../completions/you/auth/YouAuthenticationService.java | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5ce2eaf3..53825dc1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,10 @@ plugins { group = properties("pluginGroup").get() version = properties("pluginVersion").get() + "-" + properties("pluginSinceBuild").get() +checkstyle { + toolVersion = libs.versions.checkstyle.get() +} + repositories { mavenCentral() gradlePluginPortal() @@ -50,7 +54,7 @@ dependencies { implementation(project(":codegpt-telemetry")) implementation(project(":codegpt-treesitter")) - implementation(enforcedPlatform("com.fasterxml.jackson:jackson-bom:${libs.versions.jackson.get()}")) + implementation(enforcedPlatform(libs.jackson.bom)) implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation(libs.flexmark.all) { diff --git a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts index f42cd97f..0a578abf 100644 --- a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts @@ -22,7 +22,7 @@ intellij { } checkstyle { - toolVersion = "10.12.5" + toolVersion = "10.15.0" } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d2e3bde..13b69c36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] analytics = "3.0.0" -assertj = "3.25.3" changelog = "2.2.0" +checkstyle = "10.15.0" commons-text = "1.11.0" flexmark = "0.64.8" -gradle-intellij-plugin-version="1.17.3" +gradle-intellij-plugin-version = "1.17.3" jackson = "2.17.0" jsoup = "1.17.2" jtokkit = "1.0.0" @@ -15,13 +15,12 @@ tree-sitter = "0.22.2" [libraries] analytics = { module = "com.rudderstack.sdk.java.analytics:analytics", version.ref = "analytics" } -assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" } gradle-intellij-plugin = { module = "org.jetbrains.intellij.plugins:gradle-intellij-plugin", version.ref = "gradle-intellij-plugin-version" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } jtokkit = { module = "com.knuddels:jtokkit", version.ref = "jtokkit" } -junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } llm-client = { module = "ee.carlrobert:llm-client", version.ref = "llm-client" } tree-sitter = { module = "io.github.bonede:tree-sitter", version.ref = "tree-sitter" } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java index a7d195ed..f6e20b72 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java @@ -60,7 +60,6 @@ public final class YouAuthenticationService { if (response.code() == 200) { try { - var messageBus = ApplicationManager.getApplication().getMessageBus(); var userManager = YouUserManager.getInstance(); var authenticationResponse = @@ -72,6 +71,7 @@ public final class YouAuthenticationService { YouApiClient.getInstance().getSubscription(authenticationResponse); var subscribed = subscription != null && "youpro".equals(subscription.getService()); userManager.setSubscribed(subscribed); + var messageBus = ApplicationManager.getApplication().getMessageBus(); if (subscribed) { messageBus.syncPublisher(YouSubscriptionNotifier.SUBSCRIPTION_TOPIC).subscribed(); } From 3c2d185b8a949354479c7e3ecaa5d14e956ad159 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 7 Apr 2024 17:28:39 +0300 Subject: [PATCH 19/20] 2.6.0 --- CHANGELOG.md | 5 ++++- gradle.properties | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaf18c1..b5ebbcd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.6.0-233] - 2024-04-07 + ### Added - Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models @@ -414,7 +416,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OPENAI_API_KEY` persistence, key is saved in the OS password safe from now on -[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...HEAD +[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.0-233...HEAD +[2.6.0-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...v2.6.0-233 [2.5.1]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.0...v2.5.1 [2.5.0]: https://github.com/carlrobertoh/CodeGPT/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/carlrobertoh/CodeGPT/compare/v2.3.1...v2.4.0 diff --git a/gradle.properties b/gradle.properties index aed14e03..c3a537b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = ee.carlrobert pluginName = CodeGPT pluginRepositoryUrl = https://github.com/carlrobertoh/CodeGPT # SemVer format -> https://semver.org -pluginVersion = 2.5.1 +pluginVersion = 2.6.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 233 From 3ef9aba4880779376cc70980e2e6f576bbba96c5 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 7 Apr 2024 17:38:00 +0300 Subject: [PATCH 20/20] docs: extract getting started guide into its own doc --- GETTING_STARTED.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 73 +-------------------------------------- 2 files changed, 87 insertions(+), 72 deletions(-) create mode 100644 GETTING_STARTED.md diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 00000000..02558808 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,86 @@ +## Getting Started + +1. **Download the Plugin** + +2. **Choose Your Preferred Service**: + + a) **OpenAI** - Requires authentication via OpenAI API key. + + b) **Azure** - Requires authentication via Active Directory or API key. + + c) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, + Groq, Ollama and many more. + + d) **Anthropic** - Requires authentication via API key. + + e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. + + f) **LLaMA C/C++ Port** - Recommended to have a decent computer to handle the computational requirements of running + inference. + > **Note**: Currently supported only on Linux and MacOS. + +3. **Start Using the Features** + +### Installation + +The plugin is available from [JetBrains Marketplace][plugin-repo]. +You can install it directly from your IDE via the `File | Settings/Preferences | Plugins` screen. +On the `Marketplace` tab simply search for `codegpt` and select the `CodeGPT` suggestion: + +![marketplace][marketplace-img] + +### OpenAI + +After successful installation, configure your API key. Navigate to the plugin's settings via **File | +Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. + +### Azure OpenAI + +For Azure OpenAI services, you'll need to input three additional fields: + +- **Resource name**: The name of your Azure OpenAI Cognitive Services. It's the first part of the url you're provided to + use the service: "https://**my-resource-name**.openai.azure.com/". You can find it in your Azure Cognitive Services + page, under `Resource Management` → `Resource Management` → `Keys and Endpoints`. +- **Deployment ID**: The name of your Deployment. You can find it in the Azure AI Studio, + under `Management` → `Deployment` → `Deployment Name` column in the table. +- **API version**: The most recent non-preview version. + +In addition to these, you need to input one of the two API Keys provided, found along with the `Resource Name`. + +### You.com (Free) + +**You.com** is a search engine that summarizes the best parts of the internet for **you**, with private ads and with +privacy options. + +**You⚡Pro** + +Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. + +Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more +details. + +### LLaMA C/C++ Port (Free, Local) + +> **Note**: Currently supported only on Linux and MacOS. + +The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. + +#### Getting Started + +1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. + Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. + +2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` + button. A status message will be displayed, indicating that the server is starting up. + +3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on + the `Apply/OK` button to save your settings and start using the application. + +animated + +> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the +> port and click `Apply/OK`. + +[marketplace-img]: https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/marketplace.png?raw=true + +[plugin-repo]: https://plugins.jetbrains.com/plugin/21056-codegpt diff --git a/README.md b/README.md index c39d590b..8db07bdd 100644 --- a/README.md +++ b/README.md @@ -84,78 +84,7 @@ CodeGPT supports a completely offline development workflow by allowing you to co ![Offline Development Support](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/llama-settings.png?raw=true) -## Getting Started - -1. **Download the Plugin** - -2. **Choose Your Preferred Service**: - - a) **OpenAI** - Requires authentication via OpenAI API key. - - b) **Azure** - Requires authentication via Active Directory or API key. - - c) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, Groq, Ollama and many more. - - d) **Anthropic** - Requires authentication via API key. - - e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. - - f) **LLaMA C/C++ Port** - Recommended to have a decent computer to handle the computational requirements of running inference. - > **Note**: Currently supported only on Linux and MacOS. - -3. **Start Using the Features** - -### Installation - -The plugin is available from [JetBrains Marketplace][plugin-repo]. -You can install it directly from your IDE via the `File | Settings/Preferences | Plugins` screen. -On the `Marketplace` tab simply search for `codegpt` and select the `CodeGPT` suggestion: - -![marketplace][marketplace-img] - -### OpenAI - -After successful installation, configure your API key. Navigate to the plugin's settings via **File | Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. - -### Azure OpenAI - -For Azure OpenAI services, you'll need to input three additional fields: - -- **Resource name**: The name of your Azure OpenAI Cognitive Services. It's the first part of the url you're provided to use the service: "https://**my-resource-name**.openai.azure.com/". You can find it in your Azure Cognitive Services page, under `Resource Management` → `Resource Management` → `Keys and Endpoints`. -- **Deployment ID**: The name of your Deployment. You can find it in the Azure AI Studio, under `Management` → `Deployment` → `Deployment Name` column in the table. -- **API version**: The most recent non-preview version. - -In addition to these, you need to input one of the two API Keys provided, found along with the `Resource Name`. - -### You.com (Free) - -**You.com** is a search engine that summarizes the best parts of the internet for **you**, with private ads and with privacy options. - -**You⚡Pro** - -Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. - -Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more details. - -### LLaMA C/C++ Port (Free, Local) - -> **Note**: Currently supported only on Linux and MacOS. - -The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. - -#### Getting Started - -1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. - -2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` button. A status message will be displayed, indicating that the server is starting up. - -3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on the `Apply/OK` button to save your settings and start using the application. - -animated - -> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the port and click `Apply/OK`. - -### Running locally +## Running locally **Linux or macOS** ```shell