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("", 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("", 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
zcmZ`+c{tSF+aFmY6vf!WSjRGUF~iu`Y*~JmvdtKov1Vo%G4||~J-eb(XtR`^Od(r}
z3|T{264|o*jh^0~uJ^s(b6ubBeV@<0eb0UFe@?uGnE^8+HzNQ5U^X(;wWLUfV=&NB
zz9iP>L;&Cv9Ic~cVWguYVBzoUj`l(U0EY3Iw)A#ZT^xm%({5>cP|=h|Ptc@tYZ^01
zhi`z$3`XG=h*LCYVD)8Fqk1%6^N->X^rRHB{aKzZy)(2ZM=5gV1#t0F^IB87@ArfC
z{+y$H)j%IEz@XVR-Me1aeBr#Vg({-tA~+nJW41`eu>X*n=+Zy^CE@m^OVfbScRMRv
z-#IpJ1dKa9+U!3(!f>mFLO21!jGwKYAQ$LA>j0)~uB5840`6v9$SV{}Ly*I>fT9dP
z5MZb6EwDRVmFT-;vt!!xO;J+=3>TGMxQk}Tzg1%LR54Tk4+M=DW6Ps7$!s9aC;
zXz~j8q%hd7K14?fD3knd6w^kk1@MyUPs@DyO7})0L^<)n8*#*)O*DeTs?B*S4t4F`
zv-8H!Kj$zW40DClYV%u3_rzU{zJ20ZP_uyG34~1?pMMpi{VW{Gq+oYO2aYgGQVhe!
zA-7+-gw?^bibPowE^TMe&a(7!e##egF%VykXiR!}I*9m`17eV+kOhqq)@7`*M>CQ--<-}P;_SnANYdbYTR)}F8
zyioLQVnX`YcuseJ?ULD@DM?Kgmk59+Jsa7w&HC0!8gC*qakZ#XjKMD)SV|3lYAdTF
z#D-GWCM40`m@<#s76>Hdw^U@at_dFDZ)fr^Yvop4a8?E}sohYakB;!#b_Vf?@TOz|
z3N`gtZ_e|QU&>B;kxLJb3i%N&3MQS=lE5wt-TKj6Vsf{Dnx_Qm&UpI~$LN2)YM&lp
zRTj&S2tL(2)1Wa8zEiMz+b{i{(MttA3*kd>_qAg7MS3tMbr&h^cd=|CVQ*%_f=^-w
zsuJ>j8Y0WW{_{qvqhPn?taK&gf!K0O?^JC0omC-N+NCvy=lmnPUsE5|K6>0P@!sLc
zjz8$}!}d*g=AZk#MVmUCL94u5RbsV3SX@De8ItoE(vG?qDfUdi;v5X}y1jfU-V4vH
zmzX-2PK&2Gio1YA-OKF3SKzxL8SDETqO6JW{
z%pd^6Ntv~d;XIF#VCn(%L7((CHDDz@Dk{pL1$#$~ioC+&dn7ce>PZ)Z7pL}{X0QeI
zWn*7%Xoj1gR3bcBpb~PStC$YS59e7u5h`#xI@0hoeOjyAZ6e*3uLxPbo)ha@a^kd-
ztqvAc$`O?=Vq|KTa5}$J`E*}fQGVyfXnsyDF3`znY7pq*LJZp4Wem&%I!R}kNG#X1
zfpM&CanV9@hHPn(r9#@ekv-ZR0{1#4N>7YL4hrn03U#7qLGM$>w9X4Qb$ZUSC)|-J
zO#5BZy>!{J0N(P#;!XKZf%+u|Lq;ydhwxh!xV(
z4~~84A;U^X%b7A7>PWt}^j6U%u~PQ?>Her&jdqQEO%%p<
zaCfk4F>6r@pR&ulX}l@4$hSz%t(Yvwoyx7seNia@ePwWdKp5?Sp~I{xa$a`|q9?a%w)#nkJaik-41jGO41=yzW>
zSB6#};trB`uWs(?W0E(xyZG$o*IGLp=*l80wUXLQqAKYs>8NA$W7=Xs?VB-IPwge)
zlJ=A2lRp9rWEYk0bL|0{l7@jrilJu1_Z1dKHvF$MHBFLQg-N=I=dVk9XR97&q8rhv
z3r-*C-Onn-*yoOLSGiY(Vv+0DzEt&i_O3YGk_K{caV$yoU2c3){oJJbira|G2=boi
z>spEGM>X}nrZrnNh3<~t#P%4E_NQIXM_jN&95wiw-lvt-O^Ddhx{;ILl*u(WK}l;s
z5ndbhN9|6p3(-Zs&Ynk|S{;I8cRKI1P0x9(4|J8-roc6J^GeE*wKK{c!AQl9=oB#b
z2gNTn>6Prm58p4uC=97~20g}>tV?pjIGelmvp+pn9KQBA@k*X!OMP8K{P^P7_3_?u
ztFiJi7|~l<$JN%=SP6%7sFlpQn(p$e6+?E1WN&0cqiK`s
z#N|d9aUKqa=MkYq(-76>q2`+hJ^O^vPX~o^-nVml#(T04Qw|YF?T0J&g%u+eR#bMH
zews#_my6w9LR|Ew+66uoZxoLfAG!=r<+MG}W)O50l+=NeW4s^@G2{G;=b^A_5fc$9
zavmn(tO$c+xZ7_NQC?A
z{G0b)o5hcx+k^CJ_ymnL5tax>A=pTVSET6N=%4Xa@#So=j+^H-g7@cy4}`TWk@8?w
z!Jy!^*67xgo?O0$KYry$ndMgHSJh;KK*$<>TlpkRo
zfU&`7WWRRoAhxZ~R8DWMHSLsd_rGua@Sl5|rNIxLn631#_KJ7Uvp=Od7ipU#n3E?z
zc~#GSxs$uctY;#xBqKcyt6K){VXHM!%5{*fadZ=t;J}-$=hp@}w5XUgs&jS~WPG~)
zfL3GnvdG6YXW^S`#5Kuv4HNU*p-(kSH0|C&+XLRDarHAsF^1*E_s{lu6{6%l4=W6+
za;jeRXniT|Ll=4~2TqTD4{BQv=zGx9pL-OXiX3=!_XQIhr=qmt6%}9CVVSm;L5^~c
z#6%cJko0IZ&{@iEtd!n>k
z?Mwn&hP;L+o$3#kSf*3@3glpyF=L*X0IqKiPKh6lF~Nc6V??}pYvCK{UcGcMekQ7u*za%A!iO{3q4lkIfF@w#(WF
z_&Q8dY|anEoA+tvNUzwV#F)%k^4SU&VFjV}guz$uiRD7rNTAbrZ_D1!MB`rco(St8e}=ju!R{comp>Rb
z4!x*>mA4tl>}?>x(;0xn9m-CD0hX
zSIT~FZaU*(a@Ws%joE{BE<+o~*8ZJ=)q1XJ={x
zMf!R}T-|&TCG-YjR2*n-YA@_0KwbK2MZ;rgMN8HDf%%j0}}Yx%HjrE+#ga3O~zl;8D{2iz!bA0N*y6{hnf3cJv!x`0N{@fRwv1|0H
z6y*we(Yof=6iFGdU+Z|p#3?wI$1q?RsG^JmfZa$}%Ub*#A|u^)w?CX-_Px{b;2j;&
id>&k(C})n%$iIy$W{f+QKu2DR5n!Zeru$giCGx+Hz33MJ
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.
+
- 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
+
-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:
+
-- **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)
+
-**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.
+
-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.
+
-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.
-
+## 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.
-
-
-
-### Select and Ask
-
-Ask anything related to your selected code.
-
-
-
-### Replace Generated Code
-
-Instantly replace a selected code block in the editor with suggested code generated by AI.
-
-
-
-### Regenerate Response
-
-Expected a different answer? Re-generate any response of your choosing.
-
-
-
-## 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
-
-
-
+Receive single-line or whole-function autocomplete suggestions as you type.
-### Chat With Multiple Files
+
-
-
-
+> **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.
-
-
-
+
-### 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
+
-- **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.
+
+
+
+### 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.
+
+
+
+### 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.
+
+
## 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
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
literal 63721
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
z