fix: preserve agent context tags when submitting from landing view

This commit is contained in:
Carl-Robert Linnupuu 2026-04-01 01:46:38 +01:00
parent 7b6a8092f5
commit ec23affee8
15 changed files with 498 additions and 280 deletions

View file

@ -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() {

View file

@ -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 {

View file

@ -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;
});
}
}

View file

@ -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();
});

View file

@ -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(

View file

@ -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)
}
)

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -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))
}
}
}

View file

@ -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
)
}

View file

@ -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"))

View file

@ -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()

View file

@ -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
}

View file

@ -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))

View file

@ -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))
)
}
}