mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-16 19:44:36 +00:00
fix: preserve agent context tags when submitting from landing view
This commit is contained in:
parent
7b6a8092f5
commit
ec23affee8
15 changed files with 498 additions and 280 deletions
|
|
@ -17,6 +17,7 @@ import ee.carlrobert.codegpt.completions.ConversationType;
|
|||
import ee.carlrobert.codegpt.conversations.Conversation;
|
||||
import ee.carlrobert.codegpt.conversations.message.Message;
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings;
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
|
@ -67,7 +68,8 @@ public final class ChatToolWindowContentManager {
|
|||
.ifPresentOrElse(
|
||||
title -> chatPanel.getChatTabbedPane()
|
||||
.setSelectedIndex(chatPanel.getChatTabbedPane().indexOfTab(title)),
|
||||
() -> chatPanel.createAndSelectConversationTab(conversation)));
|
||||
() -> chatPanel.createAndSelectConversationTab(
|
||||
new ToolWindowInitialState(conversation))));
|
||||
}
|
||||
|
||||
public ChatToolWindowTabPanel createNewTabPanel() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.intellij.openapi.options.ShowSettingsUtil;
|
|||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.SimpleToolWindowPanel;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.ui.components.ActionLink;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import com.intellij.util.ui.JBUI.CurrentTheme.Link;
|
||||
|
|
@ -23,18 +22,18 @@ import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction;
|
|||
import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction;
|
||||
import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction;
|
||||
import ee.carlrobert.codegpt.completions.ConversationType;
|
||||
import ee.carlrobert.codegpt.conversations.Conversation;
|
||||
import ee.carlrobert.codegpt.conversations.ConversationService;
|
||||
import ee.carlrobert.codegpt.conversations.message.Message;
|
||||
import ee.carlrobert.codegpt.psistructure.models.ClassStructure;
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType;
|
||||
import ee.carlrobert.codegpt.settings.models.ModelSettings;
|
||||
import ee.carlrobert.codegpt.settings.prompts.PersonaPromptDetailsState;
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsConfigurable;
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings;
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType;
|
||||
import ee.carlrobert.codegpt.settings.service.ProviderChangeNotifier;
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType;
|
||||
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTUserDetailsNotifier;
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.ToolWindowFooterNotification;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier;
|
||||
import java.awt.CardLayout;
|
||||
|
|
@ -55,7 +54,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
|
|||
private final JPanel centerPanel;
|
||||
private final CardLayout centerLayout;
|
||||
private final Project project;
|
||||
private ChatToolWindowTabPanel landingPanel;
|
||||
private final ChatLandingPanel landingPanel;
|
||||
|
||||
public ChatToolWindowPanel(
|
||||
@NotNull Project project,
|
||||
|
|
@ -71,16 +70,17 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
|
|||
upgradePlanLink.setExternalLinkIcon();
|
||||
upgradePlanLink.setVisible(false);
|
||||
|
||||
landingPanel = new ChatLandingPanel(project, this::submitInitialMessage);
|
||||
tabbedPane = new ChatToolWindowTabbedPane(parentDisposable);
|
||||
tabbedPane.setTabLifecycleCallbacks(this::showTabsView, this::showLandingView);
|
||||
centerLayout = new CardLayout();
|
||||
centerPanel = new JPanel(centerLayout);
|
||||
centerPanel.add(tabbedPane, TABS_CARD);
|
||||
centerPanel.add(landingPanel, LANDING_CARD);
|
||||
centerLayout.show(centerPanel, LANDING_CARD);
|
||||
|
||||
initToolWindowPanel(project);
|
||||
initializeEventListeners(project);
|
||||
showLandingView();
|
||||
Disposer.register(parentDisposable, this::disposeLandingPanel);
|
||||
}
|
||||
|
||||
private void initializeEventListeners(Project project) {
|
||||
|
|
@ -112,12 +112,17 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
|
|||
return tabbedPane;
|
||||
}
|
||||
|
||||
public ChatToolWindowTabPanel createAndSelectNewTabPanel() {
|
||||
return createAndSelectConversationTab(ConversationService.getInstance().startConversation(project));
|
||||
public ChatToolWindowTabPanel createAndSelectNewTabPanel(@NotNull ToolWindowInitialState initialState) {
|
||||
return createAndSelectConversationTab(initialState);
|
||||
}
|
||||
|
||||
public ChatToolWindowTabPanel createAndSelectConversationTab(Conversation conversation) {
|
||||
var panel = new ChatToolWindowTabPanel(project, conversation);
|
||||
public ChatToolWindowTabPanel createAndSelectNewTabPanel() {
|
||||
var conversation = ConversationService.getInstance().startConversation(project);
|
||||
return createAndSelectConversationTab(new ToolWindowInitialState(conversation));
|
||||
}
|
||||
|
||||
public ChatToolWindowTabPanel createAndSelectConversationTab(ToolWindowInitialState initialState) {
|
||||
var panel = new ChatToolWindowTabPanel(project, initialState);
|
||||
tabbedPane.addNewTab(panel);
|
||||
showTabsView();
|
||||
return panel;
|
||||
|
|
@ -139,34 +144,25 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
|
|||
}
|
||||
|
||||
public void showLandingView() {
|
||||
disposeLandingPanel();
|
||||
landingPanel = createLandingPanel();
|
||||
centerPanel.add(landingPanel.getContent(), LANDING_CARD);
|
||||
centerPanel.add(landingPanel, LANDING_CARD);
|
||||
centerLayout.show(centerPanel, LANDING_CARD);
|
||||
landingPanel.requestFocusForTextArea();
|
||||
centerPanel.revalidate();
|
||||
centerPanel.repaint();
|
||||
}
|
||||
|
||||
private ChatToolWindowTabPanel createLandingPanel() {
|
||||
var conversation = ConversationService.getInstance().createConversation();
|
||||
conversation.setProjectPath(project.getBasePath());
|
||||
return new ChatToolWindowTabPanel(project, conversation, this::promoteLandingDraftToTab);
|
||||
}
|
||||
|
||||
private void promoteLandingDraftToTab(Message message, Set<ClassStructure> psiStructure) {
|
||||
var tabPanel = createAndSelectNewTabPanel();
|
||||
tabPanel.sendMessage(message, ConversationType.DEFAULT, psiStructure);
|
||||
}
|
||||
|
||||
private void disposeLandingPanel() {
|
||||
private void submitInitialMessage(
|
||||
Message message,
|
||||
Set<ClassStructure> psiStructure,
|
||||
ToolWindowInitialState initialState
|
||||
) {
|
||||
if (landingPanel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
centerPanel.remove(landingPanel.getContent());
|
||||
Disposer.dispose(landingPanel);
|
||||
landingPanel = null;
|
||||
var tabPanel = createAndSelectNewTabPanel(initialState);
|
||||
tabPanel.restoreDraftState(initialState);
|
||||
tabPanel.sendMessage(message, ConversationType.DEFAULT, psiStructure);
|
||||
}
|
||||
|
||||
public void clearImageNotifications(Project project) {
|
||||
|
|
@ -210,7 +206,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
|
|||
toolbar.setTargetComponent(this);
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
|
||||
private static class SelectedPersonaActionLink extends DumbAwareAction implements
|
||||
CustomComponentAction {
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import ee.carlrobert.codegpt.psistructure.models.ClassStructure;
|
|||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService;
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType;
|
||||
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureState;
|
||||
|
|
@ -52,13 +53,11 @@ import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel;
|
|||
import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel;
|
||||
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel;
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil;
|
||||
import ee.carlrobert.codegpt.ui.textarea.ConversationTagProcessor;
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FolderTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.HistoryTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.PersonaTagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails;
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager;
|
||||
|
|
@ -103,23 +102,13 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
private final PsiStructureRepository psiStructureRepository;
|
||||
private final TagManager tagManager;
|
||||
private final JPanel mcpApprovalContainer;
|
||||
private final DraftSubmitHandler draftSubmitHandler;
|
||||
private @Nullable ToolwindowChatCompletionRequestHandler requestHandler;
|
||||
private final JBLabel loadingLabel;
|
||||
private final JPanel queuedMessageContainer;
|
||||
|
||||
public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) {
|
||||
this(project, conversation, null);
|
||||
}
|
||||
|
||||
public ChatToolWindowTabPanel(
|
||||
@NotNull Project project,
|
||||
@NotNull Conversation conversation,
|
||||
@Nullable DraftSubmitHandler draftSubmitHandler
|
||||
) {
|
||||
public ChatToolWindowTabPanel(@NotNull Project project, ToolWindowInitialState initialState) {
|
||||
this.project = project;
|
||||
this.conversation = conversation;
|
||||
this.draftSubmitHandler = draftSubmitHandler;
|
||||
this.conversation = initialState.getConversation();
|
||||
this.chatSession = new ChatSession();
|
||||
conversationService = ConversationService.getInstance();
|
||||
toolWindowScrollablePanel = new ChatToolWindowScrollablePanel();
|
||||
|
|
@ -200,6 +189,15 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
totalTokensPanel.updateConversationTokens(conversation);
|
||||
}
|
||||
|
||||
public void restoreDraftState(@NotNull ToolWindowInitialState initialState) {
|
||||
tagManager.clear();
|
||||
initialState.getTags().forEach(userInputPanel::addTag);
|
||||
|
||||
if (initialState.getChatMode() != null) {
|
||||
userInputPanel.setChatMode(initialState.getChatMode());
|
||||
}
|
||||
}
|
||||
|
||||
public void addSelection(VirtualFile editorFile, SelectionModel selectionModel) {
|
||||
userInputPanel.addSelection(editorFile, selectionModel);
|
||||
}
|
||||
|
|
@ -267,8 +265,8 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
.sessionId(chatSession.getId())
|
||||
.conversationType(conversationType)
|
||||
.imageDetailsFromPath(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project))
|
||||
.referencedFiles(getReferencedFiles(selectedTags))
|
||||
.history(getHistory(getSelectedTags()))
|
||||
.referencedFiles(ChatContextSupport.getReferencedFiles(project, selectedTags))
|
||||
.history(ChatContextSupport.getHistory(getSelectedTags()))
|
||||
.psiStructure(psiStructure)
|
||||
.project(project)
|
||||
.chatMode(userInputPanel.getChatMode());
|
||||
|
|
@ -300,19 +298,17 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
private List<ReferencedFile> getReferencedFiles(List<? extends TagDetails> tags) {
|
||||
var settingsService = project.getService(ProxyAISettingsService.class);
|
||||
var visibleFiles = collectVisibleFiles(
|
||||
tags.stream()
|
||||
.map(this::getVirtualFile)
|
||||
.filter(Objects::nonNull)
|
||||
.toList(),
|
||||
settingsService
|
||||
);
|
||||
private <T extends TagDetails> Optional<T> findTagOfType(
|
||||
List<? extends TagDetails> tags,
|
||||
Class<T> tagClass) {
|
||||
return tags.stream()
|
||||
.filter(tagClass::isInstance)
|
||||
.map(tagClass::cast)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
return visibleFiles.stream()
|
||||
.map(ReferencedFile::from)
|
||||
.toList();
|
||||
private ToolApprovalMode getToolApprovalMode() {
|
||||
return ToolApprovalMode.REQUIRE_APPROVAL;
|
||||
}
|
||||
|
||||
private List<VirtualFile> collectVisibleFiles(
|
||||
|
|
@ -334,84 +330,34 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
output.add(file);
|
||||
return;
|
||||
}
|
||||
|
||||
Arrays.stream(file.getChildren())
|
||||
.forEach(child -> appendVisibleFiles(child, settingsService, output));
|
||||
}
|
||||
|
||||
private List<UUID> getConversationHistoryIds(List<? extends TagDetails> tags) {
|
||||
return tags.stream()
|
||||
.map(it -> {
|
||||
if (it instanceof HistoryTagDetails tagDetails) {
|
||||
return tagDetails.getConversationId();
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Conversation> getHistory(List<? extends TagDetails> tags) {
|
||||
return tags.stream()
|
||||
.map(it -> {
|
||||
if (it instanceof HistoryTagDetails tagDetails) {
|
||||
return ConversationTagProcessor.Companion.getConversation(
|
||||
tagDetails.getConversationId());
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private VirtualFile getVirtualFile(TagDetails tag) {
|
||||
VirtualFile virtualFile = null;
|
||||
if (tag.getSelected()) {
|
||||
if (tag instanceof FileTagDetails) {
|
||||
virtualFile = ((FileTagDetails) tag).getVirtualFile();
|
||||
} else if (tag instanceof EditorTagDetails) {
|
||||
virtualFile = ((EditorTagDetails) tag).getVirtualFile();
|
||||
} else if (tag instanceof FolderTagDetails) {
|
||||
virtualFile = ((FolderTagDetails) tag).getFolder();
|
||||
}
|
||||
|
||||
}
|
||||
return virtualFile;
|
||||
}
|
||||
|
||||
private <T extends TagDetails> Optional<T> findTagOfType(
|
||||
List<? extends TagDetails> tags,
|
||||
Class<T> tagClass) {
|
||||
return tags.stream()
|
||||
.filter(tagClass::isInstance)
|
||||
.map(tagClass::cast)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private ToolApprovalMode getToolApprovalMode() {
|
||||
return ToolApprovalMode.REQUIRE_APPROVAL;
|
||||
}
|
||||
|
||||
private void initializeConversationAttachedFiles() {
|
||||
restoreConversationAttachedFiles();
|
||||
|
||||
tagManager.addListener(new TagManagerListener() {
|
||||
@Override
|
||||
public void onTagAdded(TagDetails tag) {
|
||||
public void onTagAdded(@NotNull TagDetails tag) {
|
||||
syncConversationAttachedFiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagRemoved(TagDetails tag) {
|
||||
public void onTagRemoved(@NotNull TagDetails tag) {
|
||||
syncConversationAttachedFiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagSelectionChanged(TagDetails tag, SelectionModel selectionModel) {
|
||||
public void onTagSelectionChanged(
|
||||
@NotNull TagDetails tag,
|
||||
@NotNull SelectionModel selectionModel) {
|
||||
syncConversationAttachedFiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagUpdated(TagDetails tag) {
|
||||
public void onTagUpdated(@NotNull TagDetails tag) {
|
||||
syncConversationAttachedFiles();
|
||||
}
|
||||
});
|
||||
|
|
@ -439,10 +385,6 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
private void syncConversationAttachedFiles() {
|
||||
if (draftSubmitHandler != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var attachedFiles = collectConversationAttachedFiles();
|
||||
|
||||
if (Objects.equals(conversation.getAttachedFiles(), attachedFiles)) {
|
||||
|
|
@ -454,8 +396,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
private List<ConversationAttachedFile> collectConversationAttachedFiles() {
|
||||
return tagManager.getTags().stream()
|
||||
.filter(tag -> tag instanceof FileTagDetails || tag instanceof FolderTagDetails)
|
||||
return userInputPanel.getSelectedTags().stream()
|
||||
.sorted(Comparator.comparingLong(TagDetails::getCreatedOn))
|
||||
.map(this::toConversationAttachedFile)
|
||||
.filter(Objects::nonNull)
|
||||
|
|
@ -463,11 +404,20 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
private ConversationAttachedFile toConversationAttachedFile(TagDetails tag) {
|
||||
if (tag instanceof EditorTagDetails editorTagDetails) {
|
||||
return new ConversationAttachedFile(
|
||||
editorTagDetails.getVirtualFile().getPath(),
|
||||
tag.getSelected());
|
||||
}
|
||||
if (tag instanceof FileTagDetails fileTagDetails) {
|
||||
return new ConversationAttachedFile(fileTagDetails.getVirtualFile().getPath(), tag.getSelected());
|
||||
return new ConversationAttachedFile(
|
||||
fileTagDetails.getVirtualFile().getPath(),
|
||||
tag.getSelected());
|
||||
}
|
||||
if (tag instanceof FolderTagDetails folderTagDetails) {
|
||||
return new ConversationAttachedFile(folderTagDetails.getFolder().getPath(), tag.getSelected());
|
||||
return new ConversationAttachedFile(
|
||||
folderTagDetails.getFolder().getPath(),
|
||||
tag.getSelected());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -691,30 +641,9 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
.filter(TagDetails::getSelected)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
var messageBuilder = new MessageBuilder(project, text).withTags(appliedTags);
|
||||
|
||||
List<ReferencedFile> referencedFiles = getReferencedFiles(appliedTags);
|
||||
if (!referencedFiles.isEmpty()) {
|
||||
messageBuilder.withReferencedFiles(referencedFiles);
|
||||
}
|
||||
|
||||
List<UUID> conversationHistoryIds = getConversationHistoryIds(appliedTags);
|
||||
if (!conversationHistoryIds.isEmpty()) {
|
||||
messageBuilder.withConversationHistoryIds(conversationHistoryIds);
|
||||
}
|
||||
|
||||
String attachedImagePath = CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project);
|
||||
if (attachedImagePath != null) {
|
||||
messageBuilder.withImage(attachedImagePath);
|
||||
}
|
||||
|
||||
application.invokeLater(() -> {
|
||||
var message = messageBuilder.build();
|
||||
if (draftSubmitHandler != null) {
|
||||
draftSubmitHandler.onDraftSubmit(message, psiStructure);
|
||||
} else {
|
||||
sendMessage(message, ConversationType.DEFAULT, psiStructure);
|
||||
}
|
||||
var message = ChatContextSupport.buildMessage(project, text, appliedTags);
|
||||
sendMessage(message, ConversationType.DEFAULT, psiStructure);
|
||||
});
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
|
|
@ -811,26 +740,6 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
private JComponent getLandingView() {
|
||||
return new ChatToolWindowLandingPanel((action, locationOnScreen) -> {
|
||||
var editor = EditorUtil.getSelectedEditor(project);
|
||||
if (editor == null || !editor.getSelectionModel().hasSelection()) {
|
||||
OverlayUtil.showWarningBalloon(
|
||||
editor == null ? "Unable to locate a selected editor"
|
||||
: "Please select a target code before proceeding",
|
||||
locationOnScreen);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
var formattedCode = CompletionRequestUtil.formatCode(
|
||||
editor.getSelectionModel().getSelectedText(),
|
||||
editor.getVirtualFile().getPath());
|
||||
var message = new Message(action.getPrompt().replace("{SELECTION}", formattedCode));
|
||||
sendMessage(message, ConversationType.DEFAULT);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void displayConversation() {
|
||||
clearWindow();
|
||||
conversation.getMessages().forEach(message -> {
|
||||
|
|
@ -873,9 +782,23 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
return rootPanel;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DraftSubmitHandler {
|
||||
private JComponent getLandingView() {
|
||||
return new ChatToolWindowLandingPanel((action, locationOnScreen) -> {
|
||||
var editor = EditorUtil.getSelectedEditor(project);
|
||||
if (editor == null || !editor.getSelectionModel().hasSelection()) {
|
||||
OverlayUtil.showWarningBalloon(
|
||||
editor == null ? "Unable to locate a selected editor"
|
||||
: "Please select a target code before proceeding",
|
||||
locationOnScreen);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
void onDraftSubmit(Message message, Set<ClassStructure> psiStructure);
|
||||
var formattedCode = CompletionRequestUtil.formatCode(
|
||||
editor.getSelectionModel().getSelectedText(),
|
||||
editor.getVirtualFile().getPath());
|
||||
var message = new Message(action.getPrompt().replace("{SELECTION}", formattedCode));
|
||||
sendMessage(message, ConversationType.DEFAULT);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.intellij.util.ui.JBUI;
|
|||
import ee.carlrobert.codegpt.actions.toolwindow.RenameSessionAction;
|
||||
import ee.carlrobert.codegpt.conversations.ConversationService;
|
||||
import ee.carlrobert.codegpt.conversations.ConversationsState;
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
|
@ -203,9 +204,8 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane {
|
|||
public void resetCurrentlyActiveTabPanel(Project project) {
|
||||
tryFindActiveTabPanel().ifPresent(tabPanel -> {
|
||||
closeTabAt(getSelectedIndex());
|
||||
addNewTab(new ChatToolWindowTabPanel(
|
||||
project,
|
||||
ConversationService.getInstance().startConversation(project)));
|
||||
var conversation = ConversationService.getInstance().startConversation(project);
|
||||
addNewTab(new ChatToolWindowTabPanel(project, new ToolWindowInitialState(conversation)));
|
||||
repaint();
|
||||
revalidate();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,20 @@ import com.intellij.openapi.wm.ToolWindow
|
|||
import com.intellij.openapi.wm.ToolWindowFactory
|
||||
import com.intellij.ui.content.ContentManagerEvent
|
||||
import com.intellij.ui.content.ContentManagerListener
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.AgentToolWindowPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.history.ChatHistoryToolWindow
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
|
||||
import javax.swing.JComponent
|
||||
|
||||
data class ToolWindowInitialState @JvmOverloads constructor(
|
||||
val conversation: Conversation,
|
||||
val tags: List<TagDetails> = emptyList(),
|
||||
val chatMode: ChatMode? = null,
|
||||
)
|
||||
|
||||
class ProxyAIToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||
|
||||
override fun createToolWindowContent(
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import com.intellij.openapi.components.service
|
|||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.SimpleToolWindowPanel
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentCreditsToolbarLabel
|
||||
import java.awt.CardLayout
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
|
|
@ -92,10 +93,11 @@ class AgentToolWindowPanel(
|
|||
|
||||
private fun showLandingView() {
|
||||
disposeLandingPanel()
|
||||
landingPanel = createLandingPanel()
|
||||
centerPanel.add(landingPanel, LANDING_CARD)
|
||||
val panel = createLandingPanel()
|
||||
landingPanel = panel
|
||||
centerPanel.add(panel, LANDING_CARD)
|
||||
centerLayout.show(centerPanel, LANDING_CARD)
|
||||
landingPanel?.requestFocusForTextArea()
|
||||
panel.requestFocusForTextArea()
|
||||
centerPanel.revalidate()
|
||||
centerPanel.repaint()
|
||||
creditsLabel.refresh()
|
||||
|
|
@ -109,9 +111,14 @@ class AgentToolWindowPanel(
|
|||
return AgentToolWindowTabPanel(
|
||||
project = project,
|
||||
agentSession = draftSession,
|
||||
draftSubmitHandler = { message ->
|
||||
initialMessageSubmitHandler = { message ->
|
||||
val initialState = ToolWindowInitialState(
|
||||
conversation = draftSession.conversation,
|
||||
tags = message.tags
|
||||
)
|
||||
disposeLandingPanel()
|
||||
val panel = contentManager.createNewAgentTab(draftSession)
|
||||
panel.restoreDraftState(initialState)
|
||||
panel.submitMessage(message)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.agent
|
||||
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
|
|
@ -17,9 +16,9 @@ import com.intellij.util.ui.JBUI
|
|||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.agent.*
|
||||
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
|
||||
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentService
|
||||
import ee.carlrobert.codegpt.agent.ProxyAIAgent.loadProjectInstructions
|
||||
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentService
|
||||
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
|
||||
import ee.carlrobert.codegpt.agent.history.AgentCheckpointHistoryService
|
||||
import ee.carlrobert.codegpt.agent.history.AgentCheckpointTurnSequencer
|
||||
import ee.carlrobert.codegpt.agent.history.CheckpointRef
|
||||
|
|
@ -27,15 +26,12 @@ import ee.carlrobert.codegpt.agent.rollback.RollbackService
|
|||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.conversations.message.QueuedMessage
|
||||
import ee.carlrobert.codegpt.mcp.McpTagStatusUpdater
|
||||
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.models.ModelSettings
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentToolWindowLandingPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentModelComboBoxAction
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentRuntimeOptionsComboBoxAction
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.RollbackPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.TodoListPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.ToolCallCard
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState
|
||||
import ee.carlrobert.codegpt.toolwindow.agent.ui.*
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.MessageBuilder
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository
|
||||
|
|
@ -44,12 +40,13 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel
|
|||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel
|
||||
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller
|
||||
import ee.carlrobert.codegpt.ui.components.TokenUsageCounterPanel
|
||||
import ee.carlrobert.codegpt.ui.queue.QueuedMessagePanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import ee.carlrobert.codegpt.util.EditorUtil
|
||||
import ee.carlrobert.codegpt.util.StringUtil.stripThinkingBlocks
|
||||
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
|
||||
|
|
@ -65,7 +62,7 @@ import javax.swing.JPanel
|
|||
class AgentToolWindowTabPanel(
|
||||
private val project: Project,
|
||||
private val agentSession: AgentSession,
|
||||
private val draftSubmitHandler: ((MessageWithContext) -> Unit)? = null
|
||||
private val initialMessageSubmitHandler: ((MessageWithContext) -> Unit)? = null
|
||||
) : BorderLayoutPanel(), Disposable {
|
||||
companion object {
|
||||
private const val RECOVERED_CONVERSATION_RENDER_BATCH_SIZE = 6
|
||||
|
|
@ -206,6 +203,7 @@ class AgentToolWindowTabPanel(
|
|||
)
|
||||
|
||||
init {
|
||||
project.service<McpTagStatusUpdater>().registerTagManager(conversation.id, tagManager)
|
||||
setupMessageBusSubscriptions()
|
||||
rollbackPanel = RollbackPanel(project, sessionId) {
|
||||
rollbackPanel.refreshOperations()
|
||||
|
|
@ -290,8 +288,8 @@ class AgentToolWindowTabPanel(
|
|||
private fun handleSubmit(text: String) {
|
||||
if (text.isBlank()) return
|
||||
val message = MessageWithContext(text, userInputPanel.getSelectedTags())
|
||||
if (draftSubmitHandler != null) {
|
||||
draftSubmitHandler.invoke(message)
|
||||
if (initialMessageSubmitHandler != null) {
|
||||
initialMessageSubmitHandler.invoke(message)
|
||||
return
|
||||
}
|
||||
submitMessage(message)
|
||||
|
|
@ -301,7 +299,8 @@ class AgentToolWindowTabPanel(
|
|||
if (message.text.isBlank()) return
|
||||
disposeLandingPanelIfPresent()
|
||||
scrollablePanel.clearLandingViewIfVisible()
|
||||
val agentModelSelection = service<ModelSettings>().getModelSelectionForFeature(FeatureType.AGENT)
|
||||
val agentModelSelection =
|
||||
service<ModelSettings>().getModelSelectionForFeature(FeatureType.AGENT)
|
||||
agentSession.serviceType = agentModelSelection.provider
|
||||
agentSession.modelCode = agentModelSelection.selectionId
|
||||
|
||||
|
|
@ -483,7 +482,9 @@ class AgentToolWindowTabPanel(
|
|||
.setSessionConfigOption(agentSession, optionId, value)
|
||||
}.onFailure { ex ->
|
||||
OverlayUtil.showNotification(
|
||||
"${displayExternalAgentName(agentSession.externalAgentId ?: "agent")} option update failed. ${buildExternalAgentConfigFailureMessage(ex)}",
|
||||
"${displayExternalAgentName(agentSession.externalAgentId ?: "agent")} option update failed. ${
|
||||
buildExternalAgentConfigFailureMessage(ex)
|
||||
}",
|
||||
NotificationType.ERROR
|
||||
)
|
||||
}
|
||||
|
|
@ -758,6 +759,13 @@ class AgentToolWindowTabPanel(
|
|||
|
||||
fun getConversation(): Conversation = conversation
|
||||
|
||||
fun getSelectedTags(): List<TagDetails> = userInputPanel.getSelectedTags()
|
||||
|
||||
fun restoreDraftState(state: ToolWindowInitialState) {
|
||||
tagManager.clear()
|
||||
state.tags.forEach(userInputPanel::addTag)
|
||||
}
|
||||
|
||||
fun requestFocusForTextArea() {
|
||||
userInputPanel.requestFocus()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
import ee.carlrobert.codegpt.ui.textarea.ConversationTagProcessor
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
import java.util.*
|
||||
|
||||
object ChatContextSupport {
|
||||
|
||||
@JvmStatic
|
||||
fun buildMessage(project: Project, text: String, appliedTags: List<TagDetails>): Message {
|
||||
val messageBuilder = MessageBuilder(project, text).withTags(appliedTags)
|
||||
|
||||
val referencedFiles = getReferencedFiles(project, appliedTags)
|
||||
if (referencedFiles.isNotEmpty()) {
|
||||
messageBuilder.withReferencedFiles(referencedFiles)
|
||||
}
|
||||
|
||||
val conversationHistoryIds = getConversationHistoryIds(appliedTags)
|
||||
if (conversationHistoryIds.isNotEmpty()) {
|
||||
messageBuilder.withConversationHistoryIds(conversationHistoryIds)
|
||||
}
|
||||
|
||||
CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project)?.let(messageBuilder::withImage)
|
||||
|
||||
return messageBuilder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getReferencedFiles(project: Project, tags: List<TagDetails>): List<ReferencedFile> {
|
||||
val settingsService = project.getService(ProxyAISettingsService::class.java)
|
||||
val visibleFiles = collectVisibleFiles(
|
||||
tags.mapNotNull(::getVirtualFile),
|
||||
settingsService
|
||||
)
|
||||
|
||||
return visibleFiles.map(ReferencedFile::from)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getHistory(tags: List<TagDetails>): List<Conversation> {
|
||||
return tags.mapNotNull { tag ->
|
||||
(tag as? HistoryTagDetails)?.conversationId?.let(ConversationTagProcessor.Companion::getConversation)
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
private fun getConversationHistoryIds(tags: List<TagDetails>): List<UUID> {
|
||||
return tags.mapNotNull { tag ->
|
||||
(tag as? HistoryTagDetails)?.conversationId
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectVisibleFiles(
|
||||
inputFiles: List<VirtualFile>,
|
||||
settingsService: ProxyAISettingsService
|
||||
): List<VirtualFile> {
|
||||
val visibleFiles = LinkedHashSet<VirtualFile>()
|
||||
inputFiles.forEach { appendVisibleFiles(it, settingsService, visibleFiles) }
|
||||
return visibleFiles.toList()
|
||||
}
|
||||
|
||||
private fun appendVisibleFiles(
|
||||
file: VirtualFile,
|
||||
settingsService: ProxyAISettingsService,
|
||||
output: LinkedHashSet<VirtualFile>
|
||||
) {
|
||||
if (!file.isValid || !settingsService.isVirtualFileVisible(file)) {
|
||||
return
|
||||
}
|
||||
if (!file.isDirectory) {
|
||||
output.add(file)
|
||||
return
|
||||
}
|
||||
|
||||
file.children.forEach { child ->
|
||||
appendVisibleFiles(child, settingsService, output)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVirtualFile(tag: TagDetails): VirtualFile? {
|
||||
if (!tag.selected) {
|
||||
return null
|
||||
}
|
||||
|
||||
return when (tag) {
|
||||
is FileTagDetails -> tag.virtualFile
|
||||
is EditorTagDetails -> tag.virtualFile
|
||||
is FolderTagDetails -> tag.folder
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestUtil
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.ConversationService
|
||||
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
|
||||
import ee.carlrobert.codegpt.psistructure.models.ClassStructure
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureState
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
|
||||
import ee.carlrobert.codegpt.util.EditorUtil
|
||||
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import javax.swing.Box
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
class ChatLandingPanel(
|
||||
private val project: Project,
|
||||
private val onSubmitInitialMessage: InitialMessageSubmitHandler
|
||||
) : BorderLayoutPanel(), Disposable {
|
||||
|
||||
private val tagManager = TagManager()
|
||||
private val toolWindowScrollablePanel = ChatToolWindowScrollablePanel()
|
||||
private val psiStructureRepository = PsiStructureRepository(
|
||||
this,
|
||||
project,
|
||||
tagManager,
|
||||
PsiStructureProvider(),
|
||||
CoroutineDispatchers()
|
||||
)
|
||||
private val totalTokensPanel = TotalTokensPanel(
|
||||
Conversation(),
|
||||
EditorUtil.getSelectedEditorSelectedText(project),
|
||||
this,
|
||||
psiStructureRepository
|
||||
)
|
||||
private val userInputPanel = UserInputPanel(
|
||||
project,
|
||||
totalTokensPanel,
|
||||
this,
|
||||
FeatureType.CHAT,
|
||||
tagManager,
|
||||
this::handleSubmit,
|
||||
this::handleCancel,
|
||||
true
|
||||
)
|
||||
|
||||
init {
|
||||
addToCenter(createScrollPaneWithSmartScroller(toolWindowScrollablePanel))
|
||||
addToBottom(createSouthPanel())
|
||||
toolWindowScrollablePanel.displayLandingView(getLandingView())
|
||||
userInputPanel.requestFocus()
|
||||
}
|
||||
|
||||
fun requestFocusForTextArea() {
|
||||
userInputPanel.requestFocus()
|
||||
}
|
||||
|
||||
override fun dispose() = Unit
|
||||
|
||||
private fun handleSubmit(text: String) {
|
||||
submitInitialMessage(text)
|
||||
}
|
||||
|
||||
private fun handleCancel(): Unit = Unit
|
||||
|
||||
private fun submitInitialMessage(text: String) {
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val application = ApplicationManager.getApplication()
|
||||
application.executeOnPooledThread {
|
||||
val selectedTags = userInputPanel.getSelectedTags()
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val initialState =
|
||||
ToolWindowInitialState(conversation, selectedTags, userInputPanel.getChatMode())
|
||||
val message = ChatContextSupport.buildMessage(project, text, selectedTags)
|
||||
val psiStructure = currentPsiStructure()
|
||||
|
||||
application.invokeLater {
|
||||
onSubmitInitialMessage.submitInitialMessage(message, psiStructure, initialState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentPsiStructure(): Set<ClassStructure> {
|
||||
return when (val structureState = psiStructureRepository.structureState.value) {
|
||||
is PsiStructureState.Content -> structureState.elements
|
||||
else -> emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSouthPanel(): JComponent {
|
||||
return BorderLayoutPanel()
|
||||
.addToTop(createStatusPanel())
|
||||
.addToCenter(createUserPromptPanel())
|
||||
}
|
||||
|
||||
private fun createStatusPanel(): JComponent {
|
||||
val statusPanel = JPanel(GridBagLayout())
|
||||
statusPanel.border = JBUI.Borders.empty(8)
|
||||
statusPanel.isOpaque = false
|
||||
|
||||
val gbc = GridBagConstraints()
|
||||
gbc.gridx = 0
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 1.0
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL
|
||||
statusPanel.add(Box.createHorizontalGlue(), gbc)
|
||||
|
||||
gbc.gridx = 1
|
||||
gbc.weightx = 0.0
|
||||
gbc.anchor = GridBagConstraints.EAST
|
||||
gbc.fill = GridBagConstraints.NONE
|
||||
statusPanel.add(totalTokensPanel, gbc)
|
||||
return statusPanel
|
||||
}
|
||||
|
||||
private fun createUserPromptPanel(): JComponent {
|
||||
return JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
|
||||
JBUI.Borders.empty(8)
|
||||
)
|
||||
add(userInputPanel, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLandingView(): JComponent {
|
||||
return ChatToolWindowLandingPanel { action, locationOnScreen ->
|
||||
val editor = EditorUtil.getSelectedEditor(project)
|
||||
if (editor == null || !editor.selectionModel.hasSelection()) {
|
||||
OverlayUtil.showWarningBalloon(
|
||||
if (editor == null) {
|
||||
"Unable to locate a selected editor"
|
||||
} else {
|
||||
"Please select a target code before proceeding"
|
||||
},
|
||||
locationOnScreen
|
||||
)
|
||||
return@ChatToolWindowLandingPanel
|
||||
}
|
||||
|
||||
val selectedText =
|
||||
editor.selectionModel.selectedText ?: return@ChatToolWindowLandingPanel
|
||||
val formattedCode = CompletionRequestUtil.formatCode(
|
||||
selectedText,
|
||||
editor.virtualFile.path
|
||||
)
|
||||
submitInitialMessage(action.prompt.replace("{SELECTION}", formattedCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat
|
||||
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.psistructure.models.ClassStructure
|
||||
import ee.carlrobert.codegpt.toolwindow.ToolWindowInitialState
|
||||
|
||||
fun interface InitialMessageSubmitHandler {
|
||||
fun submitInitialMessage(
|
||||
message: Message,
|
||||
psiStructure: Set<ClassStructure>,
|
||||
initialState: ToolWindowInitialState
|
||||
)
|
||||
}
|
||||
|
|
@ -13,7 +13,9 @@ import com.intellij.openapi.application.runUndoTransparentWriteAction
|
|||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionManager
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
|
|
@ -60,7 +62,10 @@ class PromptTextField(
|
|||
private val onSubmit: (String) -> Unit,
|
||||
private val onFilesDropped: (List<VirtualFile>) -> Unit = {},
|
||||
featureType: FeatureType? = null,
|
||||
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
|
||||
document: Document = EditorFactory.getInstance().createDocument("").apply {
|
||||
IS_PROMPT_TEXT_FIELD_DOCUMENT.set(this, true)
|
||||
},
|
||||
) : EditorTextField(document, project, FileTypes.PLAIN_TEXT, false, false), Disposable {
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private val lookupManager = PromptTextFieldLookupManager(project, onLookupAdded)
|
||||
|
|
@ -80,7 +85,6 @@ class PromptTextField(
|
|||
|
||||
init {
|
||||
isOneLineMode = false
|
||||
IS_PROMPT_TEXT_FIELD_DOCUMENT.set(document, true)
|
||||
document.putUserData(PROMPT_FIELD_KEY, this)
|
||||
setPlaceholder(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"))
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ class UserInputHeaderPanel(
|
|||
|
||||
private val backgroundScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
private var purgingHiddenTags = false
|
||||
|
||||
init {
|
||||
tagManager.addListener(this)
|
||||
|
|
@ -127,10 +126,10 @@ class UserInputHeaderPanel(
|
|||
}
|
||||
|
||||
fun getSelectedTags(): List<TagDetails> =
|
||||
tagManager.getTags().filter { it.selected }.toMutableList()
|
||||
tagManager.getEffectiveTags().filter { it.selected }.toMutableList()
|
||||
|
||||
fun getLastTag(): TagDetails? {
|
||||
return tagManager.getTags()
|
||||
return tagManager.getEffectiveTags()
|
||||
.sortedWith(TagDetailsComparator())
|
||||
.lastOrNull()
|
||||
}
|
||||
|
|
@ -239,36 +238,9 @@ class UserInputHeaderPanel(
|
|||
}
|
||||
|
||||
private fun onTagsChanged() {
|
||||
if (!purgingHiddenTags) {
|
||||
val hiddenTags = tagManager.getTags().filterNot(::isTagVisible)
|
||||
if (hiddenTags.isNotEmpty()) {
|
||||
purgingHiddenTags = true
|
||||
hiddenTags.forEach { tagManager.remove(it) }
|
||||
purgingHiddenTags = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
components.filterIsInstance<TagPanel>().forEach { remove(it) }
|
||||
|
||||
val allTags = tagManager.getTags()
|
||||
|
||||
val filesVirtualFilesSet = allTags
|
||||
.filterIsInstance<FileTagDetails>()
|
||||
.map { it.virtualFile }
|
||||
.toSet()
|
||||
|
||||
/**
|
||||
* Filter the tags collection to prioritize FileTagDetails over EditorTagDetails
|
||||
* Keep all tags except EditorTagDetails that have a corresponding FileTagDetails
|
||||
*/
|
||||
val tags = allTags.filter { tag ->
|
||||
if (tag is EditorTagDetails) {
|
||||
!filesVirtualFilesSet.contains(tag.virtualFile)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
val tags = tagManager.getEffectiveTags()
|
||||
.sortedWith(TagDetailsComparator())
|
||||
.toSet()
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ class TagManager {
|
|||
|
||||
fun getTags(): Set<TagDetails> = synchronized(this) { tags.toSet() }
|
||||
|
||||
fun getEffectiveTags(): Set<TagDetails> = synchronized(this) {
|
||||
tags
|
||||
.filterEffectiveFileTags()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun containsTag(file: VirtualFile): Boolean = tags.any {
|
||||
// TODO: refactor
|
||||
if (it is SelectionTagDetails) {
|
||||
|
|
@ -141,6 +147,16 @@ class TagManager {
|
|||
}
|
||||
}
|
||||
|
||||
private fun Collection<TagDetails>.filterEffectiveFileTags(): List<TagDetails> {
|
||||
val fileVirtualFiles = filterIsInstance<FileTagDetails>()
|
||||
.map { it.virtualFile }
|
||||
.toSet()
|
||||
|
||||
return filter { tag ->
|
||||
tag !is EditorTagDetails || tag.virtualFile !in fileVirtualFiles
|
||||
}
|
||||
}
|
||||
|
||||
interface McpTagUpdateListener {
|
||||
fun updateMcpTagInPlace(tagDetails: McpTagDetails): Boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import ee.carlrobert.codegpt.settings.models.ModelSettings
|
|||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import testsupport.IntegrationTest
|
||||
import testsupport.http.RequestEntity
|
||||
|
|
@ -36,7 +35,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
|
||||
val message = Message("Hello!")
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val panel = ChatToolWindowTabPanel(project, conversation)
|
||||
val panel = ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation))
|
||||
expectOpenAIStreamingHello { promptText ->
|
||||
assertThat(promptText).contains("TEST_SYSTEM_PROMPT")
|
||||
assertThat(promptText).contains("Hello!")
|
||||
|
|
@ -55,7 +54,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
fun testSendingMessageWithReferencedFilesAddsFileContextToPrompt() {
|
||||
val message = Message("Explain referenced files")
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val panel = ChatToolWindowTabPanel(project, conversation)
|
||||
val panel = ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation))
|
||||
panel.includeFiles(
|
||||
listOf(
|
||||
LightVirtualFile("A.kt", "fun a() = 1"),
|
||||
|
|
@ -82,7 +81,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
|
||||
val message = Message("Fix compile errors in this class")
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val panel = ChatToolWindowTabPanel(project, conversation)
|
||||
val panel = ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation))
|
||||
expectOpenAIStreamingHello { promptText ->
|
||||
assertThat(promptText).contains("FIX_ERRORS_SYSTEM_PROMPT")
|
||||
assertThat(promptText).contains("Fix compile errors in this class")
|
||||
|
|
@ -102,7 +101,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
|
||||
val message = Message("What is in this image?")
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val panel = ChatToolWindowTabPanel(project, conversation)
|
||||
val panel = ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation))
|
||||
expectOpenAIStreamingHello { promptText ->
|
||||
assertThat(promptText).contains("What is in this image?")
|
||||
}
|
||||
|
|
@ -121,7 +120,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
}
|
||||
val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tempFile)
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
val panel = ChatToolWindowTabPanel(project, conversation)
|
||||
val panel = ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation))
|
||||
|
||||
panel.includeFiles(listOf(virtualFile))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,65 +9,65 @@ import java.awt.event.ActionEvent
|
|||
|
||||
class ChatToolWindowTabbedPaneTest : BasePlatformTestCase() {
|
||||
|
||||
fun testClearAllTabs() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
fun testClearAllTabs() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
|
||||
tabbedPane.clearAll()
|
||||
tabbedPane.clearAll()
|
||||
|
||||
assertThat(tabbedPane.activeTabMapping).isEmpty()
|
||||
}
|
||||
assertThat(tabbedPane.activeTabMapping).isEmpty()
|
||||
}
|
||||
|
||||
fun testAddingNewTabs() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
fun testAddingNewTabs() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
|
||||
assertThat(tabbedPane.activeTabMapping.keys)
|
||||
.containsExactly("Chat 1", "Chat 2", "Chat 3")
|
||||
}
|
||||
assertThat(tabbedPane.activeTabMapping.keys)
|
||||
.containsExactly("Chat 1", "Chat 2", "Chat 3")
|
||||
}
|
||||
|
||||
fun testResetCurrentlyActiveTabPanel() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
conversation.addMessage(Message("TEST_PROMPT", "TEST_RESPONSE"))
|
||||
tabbedPane.addNewTab(ChatToolWindowTabPanel(project, conversation))
|
||||
fun testResetCurrentlyActiveTabPanel() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
val conversation = ConversationService.getInstance().startConversation(project)
|
||||
conversation.addMessage(Message("TEST_PROMPT", "TEST_RESPONSE"))
|
||||
tabbedPane.addNewTab(ChatToolWindowTabPanel(project, ToolWindowInitialState(conversation)))
|
||||
|
||||
tabbedPane.resetCurrentlyActiveTabPanel(project)
|
||||
tabbedPane.resetCurrentlyActiveTabPanel(project)
|
||||
|
||||
val tabPanel = tabbedPane.activeTabMapping["Chat 1"]
|
||||
assertThat(tabPanel!!.conversation.messages).isEmpty()
|
||||
}
|
||||
val tabPanel = tabbedPane.activeTabMapping["Chat 1"]
|
||||
assertThat(tabPanel!!.conversation.messages).isEmpty()
|
||||
}
|
||||
|
||||
fun testCanCloseFirstTabWhenMultipleTabsExist() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
fun testCanCloseFirstTabWhenMultipleTabsExist() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
|
||||
tabbedPane.CloseActionListener("Chat 1")
|
||||
.actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close"))
|
||||
tabbedPane.CloseActionListener("Chat 1")
|
||||
.actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close"))
|
||||
|
||||
assertThat(tabbedPane.activeTabMapping.keys).containsExactly("Chat 2")
|
||||
assertThat(tabbedPane.tabCount).isEqualTo(1)
|
||||
}
|
||||
assertThat(tabbedPane.activeTabMapping.keys).containsExactly("Chat 2")
|
||||
assertThat(tabbedPane.tabCount).isEqualTo(1)
|
||||
}
|
||||
|
||||
fun testCanCloseLastRemainingTab() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
fun testCanCloseLastRemainingTab() {
|
||||
val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable())
|
||||
tabbedPane.addNewTab(createNewTabPanel())
|
||||
|
||||
tabbedPane.CloseActionListener("Chat 1")
|
||||
.actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close"))
|
||||
tabbedPane.CloseActionListener("Chat 1")
|
||||
.actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close"))
|
||||
|
||||
assertThat(tabbedPane.activeTabMapping).isEmpty()
|
||||
assertThat(tabbedPane.tabCount).isZero()
|
||||
}
|
||||
assertThat(tabbedPane.activeTabMapping).isEmpty()
|
||||
assertThat(tabbedPane.tabCount).isZero()
|
||||
}
|
||||
|
||||
private fun createNewTabPanel(): ChatToolWindowTabPanel {
|
||||
return ChatToolWindowTabPanel(
|
||||
project,
|
||||
ConversationService.getInstance().startConversation(project)
|
||||
)
|
||||
}
|
||||
private fun createNewTabPanel(): ChatToolWindowTabPanel {
|
||||
return ChatToolWindowTabPanel(
|
||||
project,
|
||||
ToolWindowInitialState(ConversationService.getInstance().startConversation(project))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue