From ec23affee8d6c9718efea89f3d723daecb1f9d27 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 1 Apr 2026 01:46:38 +0100 Subject: [PATCH] fix: preserve agent context tags when submitting from landing view --- .../chat/ChatToolWindowContentManager.java | 4 +- .../toolwindow/chat/ChatToolWindowPanel.java | 54 +++-- .../chat/ChatToolWindowTabPanel.java | 203 ++++++------------ .../chat/ChatToolWindowTabbedPane.java | 6 +- .../toolwindow/ProxyAIToolWindowFactory.kt | 9 + .../toolwindow/agent/AgentToolWindowPanel.kt | 19 +- .../agent/AgentToolWindowTabPanel.kt | 40 ++-- .../toolwindow/chat/ChatContextSupport.kt | 98 +++++++++ .../toolwindow/chat/ChatLandingPanel.kt | 171 +++++++++++++++ .../chat/InitialMessageSubmitHandler.kt | 13 ++ .../codegpt/ui/textarea/PromptTextField.kt | 8 +- .../textarea/header/UserInputHeaderPanel.kt | 34 +-- .../ui/textarea/header/tag/TagManager.kt | 16 ++ .../chat/ChatToolWindowTabPanelTest.kt | 11 +- .../chat/ChatToolWindowTabbedPaneTest.kt | 92 ++++---- 15 files changed, 498 insertions(+), 280 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatContextSupport.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatLandingPanel.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/InitialMessageSubmitHandler.kt diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java index be1dfb26..85173b52 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java @@ -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() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java index e0ff9a84..13ec35df 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -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 psiStructure) { - var tabPanel = createAndSelectNewTabPanel(); - tabPanel.sendMessage(message, ConversationType.DEFAULT, psiStructure); - } - - private void disposeLandingPanel() { + private void submitInitialMessage( + Message message, + Set 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 { 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 8f10b577..1de0c9c4 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -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 getReferencedFiles(List tags) { - var settingsService = project.getService(ProxyAISettingsService.class); - var visibleFiles = collectVisibleFiles( - tags.stream() - .map(this::getVirtualFile) - .filter(Objects::nonNull) - .toList(), - settingsService - ); + private Optional findTagOfType( + List tags, + Class 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 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 getConversationHistoryIds(List tags) { - return tags.stream() - .map(it -> { - if (it instanceof HistoryTagDetails tagDetails) { - return tagDetails.getConversationId(); - } - return null; - }) - .filter(Objects::nonNull) - .toList(); - } - - private List getHistory(List 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 Optional findTagOfType( - List tags, - Class 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 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 referencedFiles = getReferencedFiles(appliedTags); - if (!referencedFiles.isEmpty()) { - messageBuilder.withReferencedFiles(referencedFiles); - } - - List 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 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; + }); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java index a92a6163..1c2c73dc 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java @@ -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(); }); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ProxyAIToolWindowFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ProxyAIToolWindowFactory.kt index 3bb03e5f..735bb1b5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ProxyAIToolWindowFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ProxyAIToolWindowFactory.kt @@ -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 = emptyList(), + val chatMode: ChatMode? = null, +) + class ProxyAIToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt index 3bfdc955..84f5c483 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt @@ -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) } ) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt index 3ce065d3..fd0992ae 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt @@ -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().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().getModelSelectionForFeature(FeatureType.AGENT) + val agentModelSelection = + service().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 = userInputPanel.getSelectedTags() + + fun restoreDraftState(state: ToolWindowInitialState) { + tagManager.clear() + state.tags.forEach(userInputPanel::addTag) + } + fun requestFocusForTextArea() { userInputPanel.requestFocus() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatContextSupport.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatContextSupport.kt new file mode 100644 index 00000000..511bef6d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatContextSupport.kt @@ -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): 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): List { + val settingsService = project.getService(ProxyAISettingsService::class.java) + val visibleFiles = collectVisibleFiles( + tags.mapNotNull(::getVirtualFile), + settingsService + ) + + return visibleFiles.map(ReferencedFile::from) + } + + @JvmStatic + fun getHistory(tags: List): List { + return tags.mapNotNull { tag -> + (tag as? HistoryTagDetails)?.conversationId?.let(ConversationTagProcessor.Companion::getConversation) + }.distinct() + } + + private fun getConversationHistoryIds(tags: List): List { + return tags.mapNotNull { tag -> + (tag as? HistoryTagDetails)?.conversationId + } + } + + private fun collectVisibleFiles( + inputFiles: List, + settingsService: ProxyAISettingsService + ): List { + val visibleFiles = LinkedHashSet() + inputFiles.forEach { appendVisibleFiles(it, settingsService, visibleFiles) } + return visibleFiles.toList() + } + + private fun appendVisibleFiles( + file: VirtualFile, + settingsService: ProxyAISettingsService, + output: LinkedHashSet + ) { + 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 + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatLandingPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatLandingPanel.kt new file mode 100644 index 00000000..2f19d806 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatLandingPanel.kt @@ -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 { + 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)) + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/InitialMessageSubmitHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/InitialMessageSubmitHandler.kt new file mode 100644 index 00000000..70a5166c --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/InitialMessageSubmitHandler.kt @@ -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, + initialState: ToolWindowInitialState + ) +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index 568d8c73..395a9bda 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -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) -> 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")) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt index 28d261c7..1952aef6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt @@ -118,7 +118,6 @@ class UserInputHeaderPanel( private val backgroundScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val settingsService = project.service() - private var purgingHiddenTags = false init { tagManager.addListener(this) @@ -127,10 +126,10 @@ class UserInputHeaderPanel( } fun getSelectedTags(): List = - 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().forEach { remove(it) } - val allTags = tagManager.getTags() - - val filesVirtualFilesSet = allTags - .filterIsInstance() - .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() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt index f034bf31..33db398f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt @@ -20,6 +20,12 @@ class TagManager { fun getTags(): Set = synchronized(this) { tags.toSet() } + fun getEffectiveTags(): Set = 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.filterEffectiveFileTags(): List { + val fileVirtualFiles = filterIsInstance() + .map { it.virtualFile } + .toSet() + + return filter { tag -> + tag !is EditorTagDetails || tag.virtualFile !in fileVirtualFiles + } +} + interface McpTagUpdateListener { fun updateMcpTagInPlace(tagDetails: McpTagDetails): Boolean } diff --git a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt index ced0e3af..f3727957 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt @@ -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)) diff --git a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt index d0c86271..f11a7ce7 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt @@ -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)) + ) + } }