From 0fffa2eac26ad7219e1481fbc1f14ae8eabc2359 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 10 Mar 2026 14:30:49 +0000 Subject: [PATCH] fix: defer toolwindow tab creation and allow closing first/last tab (relates #865) --- .../toolwindow/RenameSessionAction.java | 4 +- .../chat/ChatToolWindowContentManager.java | 37 ++---- .../toolwindow/chat/ChatToolWindowPanel.java | 94 ++++++++++--- .../chat/ChatToolWindowTabPanel.java | 23 +++- .../chat/ChatToolWindowTabbedPane.java | 77 ++++++++--- .../ui/ChatToolWindowScrollablePanel.java | 41 ------ .../codegpt/settings/models/ModelProvider.kt | 1 - .../agent/AgentToolWindowContentManager.kt | 2 - .../toolwindow/agent/AgentToolWindowPanel.kt | 58 ++++++++- .../agent/AgentToolWindowTabPanel.kt | 28 ++-- .../agent/AgentToolWindowTabbedPane.kt | 123 +++++++++++------- .../agent/ui/AgentToolWindowLandingPanel.kt | 34 +++-- .../toolwindow/chat/ChatToolWindowListener.kt | 6 +- .../ui/ChatToolWindowLandingPanel.kt | 58 ++++++++- .../chat/ChatToolWindowTabbedPaneTest.kt | 24 ++++ 15 files changed, 426 insertions(+), 184 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/RenameSessionAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/RenameSessionAction.java index 725a233e..8f38f61e 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/RenameSessionAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/RenameSessionAction.java @@ -9,7 +9,7 @@ public class RenameSessionAction { private static final int MAX_NAME_LENGTH = 50; public static void renameSession(ChatToolWindowTabbedPane tabbedPane, int tabIndex) { - if (tabIndex <= 0) { + if (tabIndex < 0) { return; } @@ -44,4 +44,4 @@ public class RenameSessionAction { return checkInput(inputString); } } -} \ No newline at end of file +} 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 3ad9228f..be1dfb26 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java @@ -15,8 +15,6 @@ import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.ConversationType; import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.ConversationService; -import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.prompts.PromptsSettings; import java.util.Arrays; @@ -48,13 +46,12 @@ public final class ChatToolWindowContentManager { .getState() .getChatActions() .getStartInNewWindow(); - if (startInNewWindow || ConversationsState.getCurrentConversation() == null) { + if (startInNewWindow) { createNewTabPanel().sendMessage(message, conversationType); return; } - tryFindChatTabbedPane() - .map(tabbedPane -> tabbedPane.tryFindActiveTabPanel().orElseGet(this::createNewTabPanel)) + tryFindActiveChatTabPanel() .orElseGet(this::createNewTabPanel) .sendMessage(message, conversationType); } @@ -65,23 +62,18 @@ public final class ChatToolWindowContentManager { public void displayConversation(@NotNull Conversation conversation) { displayChatTab(); - tryFindChatTabbedPane() - .ifPresent(tabbedPane -> tabbedPane.tryFindTabTitle(conversation.getId()) - .ifPresentOrElse( - title -> tabbedPane.setSelectedIndex(tabbedPane.indexOfTab(title)), - () -> tabbedPane.addNewTab(new ChatToolWindowTabPanel(project, conversation)))); + tryFindChatToolWindowPanel().ifPresent(chatPanel -> chatPanel.getChatTabbedPane() + .tryFindTabTitle(conversation.getId()) + .ifPresentOrElse( + title -> chatPanel.getChatTabbedPane() + .setSelectedIndex(chatPanel.getChatTabbedPane().indexOfTab(title)), + () -> chatPanel.createAndSelectConversationTab(conversation))); } public ChatToolWindowTabPanel createNewTabPanel() { displayChatTab(); - return tryFindChatTabbedPane() - .map(item -> { - var panel = new ChatToolWindowTabPanel( - project, - ConversationService.getInstance().startConversation(project)); - item.addNewTab(panel); - return panel; - }) + return tryFindChatToolWindowPanel() + .map(ChatToolWindowPanel::createAndSelectNewTabPanel) .orElseThrow(); } @@ -113,12 +105,7 @@ public final class ChatToolWindowContentManager { } public void resetAll() { - tryFindChatTabbedPane().ifPresent(tabbedPane -> { - tabbedPane.clearAll(); - tabbedPane.addNewTab(new ChatToolWindowTabPanel( - project, - ConversationService.getInstance().startConversation(project))); - }); + tryFindChatTabbedPane().ifPresent(ChatToolWindowTabbedPane::clearAll); } public @NotNull ToolWindow getToolWindow() { @@ -142,4 +129,4 @@ public final class ChatToolWindowContentManager { public void clearAllTags() { tryFindActiveChatTabPanel().ifPresent(ChatToolWindowTabPanel::clearAllTags); } -} \ No newline at end of file +} 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 f3d3d304..e0ff9a84 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -22,9 +22,11 @@ import ee.carlrobert.codegpt.CodeGPTKeys; 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.ConversationsState; +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; @@ -35,16 +37,25 @@ import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTUserDetailsNotifier; import ee.carlrobert.codegpt.toolwindow.chat.ui.ToolWindowFooterNotification; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier; +import java.awt.CardLayout; import java.nio.file.Path; +import java.util.Set; import javax.swing.JComponent; +import javax.swing.JPanel; import org.jetbrains.annotations.NotNull; public class ChatToolWindowPanel extends SimpleToolWindowPanel { + private static final String LANDING_CARD = "LANDING"; + private static final String TABS_CARD = "TABS"; + private final ToolWindowFooterNotification imageFileAttachmentNotification; private final ActionLink upgradePlanLink; private final ChatToolWindowTabbedPane tabbedPane; + private final JPanel centerPanel; + private final CardLayout centerLayout; private final Project project; + private ChatToolWindowTabPanel landingPanel; public ChatToolWindowPanel( @NotNull Project project, @@ -60,22 +71,16 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { upgradePlanLink.setExternalLinkIcon(); upgradePlanLink.setVisible(false); - var tabPanel = new ChatToolWindowTabPanel(project, getConversation()); tabbedPane = new ChatToolWindowTabbedPane(parentDisposable); - tabbedPane.addNewTab(tabPanel); + tabbedPane.setTabLifecycleCallbacks(this::showTabsView, this::showLandingView); + centerLayout = new CardLayout(); + centerPanel = new JPanel(centerLayout); + centerPanel.add(tabbedPane, TABS_CARD); initToolWindowPanel(project); initializeEventListeners(project); - - Disposer.register(parentDisposable, tabPanel); - } - - private Conversation getConversation() { - var conversation = ConversationsState.getCurrentConversation(); - if (conversation == null) { - return ConversationService.getInstance().startConversation(project); - } - return conversation; + showLandingView(); + Disposer.register(parentDisposable, this::disposeLandingPanel); } private void initializeEventListeners(Project project) { @@ -107,6 +112,63 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { return tabbedPane; } + public ChatToolWindowTabPanel createAndSelectNewTabPanel() { + return createAndSelectConversationTab(ConversationService.getInstance().startConversation(project)); + } + + public ChatToolWindowTabPanel createAndSelectConversationTab(Conversation conversation) { + var panel = new ChatToolWindowTabPanel(project, conversation); + tabbedPane.addNewTab(panel); + showTabsView(); + return panel; + } + + public void showTabsView() { + centerLayout.show(centerPanel, TABS_CARD); + } + + public void requestFocusForInput() { + tabbedPane.tryFindActiveTabPanel() + .ifPresentOrElse( + ChatToolWindowTabPanel::requestFocusForTextArea, + () -> { + if (landingPanel != null) { + landingPanel.requestFocusForTextArea(); + } + }); + } + + public void showLandingView() { + disposeLandingPanel(); + landingPanel = createLandingPanel(); + centerPanel.add(landingPanel.getContent(), 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() { + if (landingPanel == null) { + return; + } + + centerPanel.remove(landingPanel.getContent()); + Disposer.dispose(landingPanel); + landingPanel = null; + } + public void clearImageNotifications(Project project) { imageFileAttachmentNotification.hideNotification(); @@ -115,9 +177,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { private void initToolWindowPanel(Project project) { Runnable onAddNewTab = () -> { - tabbedPane.addNewTab(new ChatToolWindowTabPanel( - project, - ConversationService.getInstance().startConversation(project))); + createAndSelectNewTabPanel(); repaint(); revalidate(); }; @@ -127,7 +187,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { .addToLeft(createActionToolbar(project, tabbedPane, onAddNewTab).getComponent()) .addToRight(upgradePlanLink)); setContent(new BorderLayoutPanel() - .addToCenter(tabbedPane) + .addToCenter(centerPanel) .addToBottom(imageFileAttachmentNotification)); }); } 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 d21ea6e2..4c9c5f7a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -99,13 +99,23 @@ 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 + ) { this.project = project; this.conversation = conversation; + this.draftSubmitHandler = draftSubmitHandler; this.chatSession = new ChatSession(); conversationService = ConversationService.getInstance(); toolWindowScrollablePanel = new ChatToolWindowScrollablePanel(); @@ -614,7 +624,12 @@ public class ChatToolWindowTabPanel implements Disposable { } application.invokeLater(() -> { - sendMessage(messageBuilder.build(), ConversationType.DEFAULT, psiStructure); + var message = messageBuilder.build(); + if (draftSubmitHandler != null) { + draftSubmitHandler.onDraftSubmit(message, psiStructure); + } else { + sendMessage(message, ConversationType.DEFAULT, psiStructure); + } }); }); return Unit.INSTANCE; @@ -772,4 +787,10 @@ public class ChatToolWindowTabPanel implements Disposable { rootPanel.add(createSouthPanel(createUserPromptPanel()), BorderLayout.SOUTH); return rootPanel; } + + @FunctionalInterface + public interface DraftSubmitHandler { + + void onDraftSubmit(Message message, Set psiStructure); + } } 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 1eb68901..a92a6163 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java @@ -47,6 +47,10 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { return o1.compareToIgnoreCase(o2); }); private final Disposable parentDisposable; + private Runnable onTabsOpened = () -> { + }; + private Runnable onAllTabsClosed = () -> { + }; public ChatToolWindowTabbedPane(Disposable parentDisposable) { this.parentDisposable = parentDisposable; @@ -59,7 +63,13 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { return activeTabMapping; } + public void setTabLifecycleCallbacks(Runnable onTabsOpened, Runnable onAllTabsClosed) { + this.onTabsOpened = onTabsOpened; + this.onAllTabsClosed = onAllTabsClosed; + } + public void addNewTab(ChatToolWindowTabPanel toolWindowPanel) { + var wasEmpty = activeTabMapping.isEmpty(); var tabIndices = activeTabMapping.keySet().toArray(new String[0]); var nextIndex = 0; for (String title : tabIndices) { @@ -79,10 +89,11 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { super.insertTab(title, null, toolWindowPanel.getContent(), null, nextIndex); activeTabMapping.put(title, toolWindowPanel); super.setSelectedIndex(nextIndex); + setTabComponentAt(nextIndex, createCloseableTabButtonPanel(title)); + toolWindowPanel.requestFocusForTextArea(); - if (nextIndex > 0) { - setTabComponentAt(nextIndex, createCloseableTabButtonPanel(title)); - toolWindowPanel.requestFocusForTextArea(); + if (wasEmpty) { + onTabsOpened.run(); } Disposer.register(parentDisposable, toolWindowPanel); @@ -122,8 +133,14 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { } public void clearAll() { + if (activeTabMapping.isEmpty()) { + return; + } + + activeTabMapping.values().forEach(Disposer::dispose); removeAll(); activeTabMapping.clear(); + onAllTabsClosed.run(); } public void renameTab(int tabIndex, String newName) { @@ -142,9 +159,7 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { setTitleAt(tabIndex, uniqueName); - if (tabIndex > 0) { - setTabComponentAt(tabIndex, createCloseableTabButtonPanel(uniqueName)); - } + setTabComponentAt(tabIndex, createCloseableTabButtonPanel(uniqueName)); activeTabMapping.remove(oldTitle); activeTabMapping.put(uniqueName, panel); @@ -187,9 +202,7 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { public void resetCurrentlyActiveTabPanel(Project project) { tryFindActiveTabPanel().ifPresent(tabPanel -> { - Disposer.dispose(tabPanel); - activeTabMapping.remove(getTitleAt(getSelectedIndex())); - removeTabAt(getSelectedIndex()); + closeTabAt(getSelectedIndex()); addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation(project))); @@ -225,9 +238,7 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { public void actionPerformed(ActionEvent evt) { var tabIndex = indexOfTab(title); if (tabIndex >= 0) { - Disposer.dispose(activeTabMapping.get(title)); - removeTabAt(tabIndex); - activeTabMapping.remove(title); + closeTabAt(tabIndex); } } } @@ -238,22 +249,34 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { TabPopupMenu() { add(createPopupMenuItem("Rename Title", e -> { - if (selectedPopupTabIndex > 0) { + if (selectedPopupTabIndex >= 0) { RenameSessionAction.renameSession(ChatToolWindowTabbedPane.this, selectedPopupTabIndex); } })); addSeparator(); add(createPopupMenuItem("Close", e -> { - if (selectedPopupTabIndex > 0) { - activeTabMapping.remove(getTitleAt(selectedPopupTabIndex)); - removeTabAt(selectedPopupTabIndex); + if (selectedPopupTabIndex >= 0) { + closeTabAt(selectedPopupTabIndex); } })); add(createPopupMenuItem("Close Other Tabs", e -> { + if (selectedPopupTabIndex < 0) { + return; + } + var selectedPopupTabTitle = getTitleAt(selectedPopupTabIndex); var tabPanel = activeTabMapping.get(selectedPopupTabTitle); + if (tabPanel == null) { + return; + } - clearAll(); + activeTabMapping.entrySet().stream() + .filter(entry -> !entry.getKey().equals(selectedPopupTabTitle)) + .map(Map.Entry::getValue) + .forEach(Disposer::dispose); + + removeAll(); + activeTabMapping.clear(); addNewTab(tabPanel); })); } @@ -262,7 +285,7 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { public void show(Component invoker, int x, int y) { selectedPopupTabIndex = ChatToolWindowTabbedPane.this.getUI() .tabForCoordinate(ChatToolWindowTabbedPane.this, x, y); - if (selectedPopupTabIndex > 0) { + if (selectedPopupTabIndex >= 0) { super.show(invoker, x, y); } } @@ -273,4 +296,22 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { return menuItem; } } + + private void closeTabAt(int tabIndex) { + if (tabIndex < 0 || tabIndex >= getTabCount()) { + return; + } + + var title = getTitleAt(tabIndex); + var panel = activeTabMapping.remove(title); + if (panel != null) { + Disposer.dispose(panel); + } + + removeTabAt(tabIndex); + + if (activeTabMapping.isEmpty()) { + onAllTabsClosed.run(); + } + } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java index 7359cb64..52dcfb88 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java @@ -1,21 +1,8 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui; -import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED; - -import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout; -import com.intellij.ui.JBColor; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.credentials.CredentialsStore; -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey; -import ee.carlrobert.codegpt.settings.service.FeatureType; -import ee.carlrobert.codegpt.settings.models.ModelSettings; -import ee.carlrobert.codegpt.settings.service.ServiceType; -import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceConfigurable; import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel; -import ee.carlrobert.codegpt.ui.UIUtil; -import ee.carlrobert.codegpt.util.ApplicationUtil; import java.awt.Rectangle; import java.util.Arrays; import java.util.HashMap; @@ -38,34 +25,6 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel { clearAll(); add(landingView); landingViewVisible = true; - if (ModelSettings.getInstance().getServiceForFeature(FeatureType.CHAT) - == ServiceType.PROXYAI - && !CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.CodeGptApiKey.INSTANCE)) { - - var panel = new ResponseMessagePanel(); - panel.addContent(UIUtil.createTextPane(""" - -

- It looks like you haven't configured your API key yet. Visit ProxyAI settings to do so. -

-

- Don't have an account? Sign up to get started. -

- """, - false, - event -> { - if (ACTIVATED.equals(event.getEventType()) - && "#OPEN_SETTINGS".equals(event.getDescription())) { - ShowSettingsUtil.getInstance().showSettingsDialog( - ApplicationUtil.findCurrentProject(), - CodeGPTServiceConfigurable.class); - } else { - UIUtil.handleHyperlinkClicked(event); - } - })); - panel.setBorder(JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0)); - add(panel); - } } public ResponseMessagePanel getResponseMessagePanel(UUID messageId) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelProvider.kt index 7ad95c70..42cb4405 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelProvider.kt @@ -260,7 +260,6 @@ val Opus_4_6: LLModel = LLModel( maxOutputTokens = 64_000, ) - private val GPT5_4: LLModel = LLModel( provider = LLMProvider.OpenAI, id = "gpt-5.4", diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowContentManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowContentManager.kt index 32290392..ff29c872 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowContentManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowContentManager.kt @@ -28,8 +28,6 @@ class AgentToolWindowContentManager(private val project: Project) : Disposable { } }) - createNewAgentTab() - return tabbedPane } 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 8dcbe207..cbf9ba16 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowPanel.kt @@ -7,20 +7,38 @@ 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.toolwindow.agent.ui.AgentCreditsToolbarLabel +import java.awt.CardLayout +import java.util.UUID import javax.swing.JComponent +import javax.swing.JPanel class AgentToolWindowPanel( private val project: Project ) : SimpleToolWindowPanel(true), Disposable { + companion object { + private const val LANDING_CARD = "LANDING" + private const val TABS_CARD = "TABS" + } + private val contentManager = project.service() private val tabbedPane = contentManager.initializeTabbedPane() + private val centerLayout = CardLayout() + private val centerPanel = JPanel(centerLayout) + private var landingPanel: AgentToolWindowTabPanel? = null init { + tabbedPane.setTabLifecycleCallbacks( + onTabsOpened = { showTabsView() }, + onAllTabsClosed = { showLandingView() } + ) + centerPanel.add(tabbedPane, TABS_CARD) toolbar = createToolbar() - setContent(tabbedPane) + setContent(centerPanel) + showLandingView() } private fun createToolbar(): JComponent { @@ -66,7 +84,45 @@ class AgentToolWindowPanel( fun getTabbedPane(): AgentToolWindowTabbedPane = tabbedPane + private fun showTabsView() { + centerLayout.show(centerPanel, TABS_CARD) + } + + private fun showLandingView() { + disposeLandingPanel() + landingPanel = createLandingPanel() + centerPanel.add(landingPanel, LANDING_CARD) + centerLayout.show(centerPanel, LANDING_CARD) + landingPanel?.requestFocusForTextArea() + centerPanel.revalidate() + centerPanel.repaint() + } + + private fun createLandingPanel(): AgentToolWindowTabPanel { + val draftSession = AgentSession( + sessionId = UUID.randomUUID().toString(), + conversation = Conversation() + ) + return AgentToolWindowTabPanel( + project = project, + agentSession = draftSession, + draftSubmitHandler = { message -> + val panel = contentManager.createNewAgentTab() + panel.submitMessage(message) + } + ) + } + + private fun disposeLandingPanel() { + val current = landingPanel ?: return + centerPanel.remove(current) + Disposer.dispose(current) + landingPanel = null + } + override fun dispose() { + tabbedPane.setTabLifecycleCallbacks(onTabsOpened = {}, onAllTabsClosed = {}) + disposeLandingPanel() tabbedPane.dispose() } } 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 25d6e4a7..04eb8868 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabPanel.kt @@ -57,7 +57,8 @@ import javax.swing.JPanel class AgentToolWindowTabPanel( private val project: Project, - private val agentSession: AgentSession + private val agentSession: AgentSession, + private val draftSubmitHandler: ((MessageWithContext) -> Unit)? = null ) : BorderLayoutPanel(), Disposable { companion object { private const val RECOVERED_CONVERSATION_RENDER_BATCH_SIZE = 6 @@ -274,6 +275,16 @@ class AgentToolWindowTabPanel( private fun handleSubmit(text: String) { if (text.isBlank()) return + val message = MessageWithContext(text, userInputPanel.getSelectedTags()) + if (draftSubmitHandler != null) { + draftSubmitHandler.invoke(message) + return + } + submitMessage(message) + } + + fun submitMessage(message: MessageWithContext) { + if (message.text.isBlank()) return disposeLandingPanelIfPresent() scrollablePanel.clearLandingViewIfVisible() agentSession.serviceType = @@ -282,16 +293,12 @@ class AgentToolWindowTabPanel( val agentService = project.service() if (agentService.isSessionRunning(sessionId)) { - addQueuedMessage(text) + addQueuedMessage(message.text) userInputPanel.clearText() userInputPanel.setSubmitEnabled(true) userInputPanel.setStopEnabled(true) - agentService.submitMessage( - MessageWithContext(text, userInputPanel.getSelectedTags()), - eventHandler, - sessionId - ) + agentService.submitMessage(message, eventHandler, sessionId) return } @@ -303,11 +310,10 @@ class AgentToolWindowTabPanel( val rollbackRunId = rollbackService.startSession(sessionId) rollbackPanel.refreshOperations() - val message = MessageWithContext(text, userInputPanel.getSelectedTags()) val messagePanel = scrollablePanel.addMessage(message.id) val userPanel = UserMessagePanel( project, - MessageBuilder(project, text).withTags(userInputPanel.getSelectedTags()).build(), + MessageBuilder(project, message.text).withTags(message.tags).build(), this ) val responsePanel = ResponseMessagePanel() @@ -322,7 +328,7 @@ class AgentToolWindowTabPanel( ) responsePanel.setResponseContent(responseBody) - userPanel.addCopyAction { CopyAction.copyToClipboard(text) } + userPanel.addCopyAction { CopyAction.copyToClipboard(message.text) } messagePanel.add(userPanel) messagePanel.add(responsePanel) scrollablePanel.update() @@ -331,7 +337,7 @@ class AgentToolWindowTabPanel( runMessageId = message.id, rollbackRunId = rollbackRunId, responsePanel = responsePanel, - prompt = text + prompt = message.text ) eventHandler.resetForNewSubmission() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabbedPane.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabbedPane.kt index fb758e12..687cd2a0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabbedPane.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentToolWindowTabbedPane.kt @@ -42,6 +42,8 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), ) private var isTabActive = true + private var onTabsOpened: () -> Unit = {} + private var onAllTabsClosed: () -> Unit = {} init { tabComponentInsets = null @@ -63,6 +65,11 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), private val sessionStates = mutableMapOf() + fun setTabLifecycleCallbacks(onTabsOpened: () -> Unit, onAllTabsClosed: () -> Unit) { + this.onTabsOpened = onTabsOpened + this.onAllTabsClosed = onAllTabsClosed + } + fun updateStatusForSession(sessionId: String, status: TabStatus) { val state = sessionStates.getOrPut(sessionId) { TabState() } state.status = status @@ -129,6 +136,7 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), } fun addNewTab(toolWindowPanel: AgentToolWindowTabPanel, select: Boolean) { + val wasEmpty = activeTabMapping.isEmpty() val tabIndices = activeTabMapping.keys.toTypedArray() var nextIndex = 0 @@ -156,9 +164,13 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), sessionStates[sessionId] = TabState(status = TabStatus.STOPPED, unseen = false) - setTabComponentAt(nextIndex, createTabButtonPanel(title, nextIndex > 0, TabStatus.STOPPED)) + setTabComponentAt(nextIndex, createTabButtonPanel(title, TabStatus.STOPPED)) toolWindowPanel.requestFocusForTextArea() + if (wasEmpty) { + onTabsOpened() + } + Disposer.register(this, toolWindowPanel) } @@ -190,8 +202,17 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), } fun clearAll() { + if (activeTabMapping.isEmpty()) { + return + } + activeTabMapping.values.forEach { + project.service().removeSession(it.getSessionId()) + Disposer.dispose(it) + } + sessionStates.clear() removeAll() activeTabMapping.clear() + onAllTabsClosed() } fun renameTab(tabIndex: Int, newName: String) { @@ -213,7 +234,7 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), TabStatus.STOPPED } - setTabComponentAt(tabIndex, createTabButtonPanel(uniqueName, tabIndex > 0, currentStatus)) + setTabComponentAt(tabIndex, createTabButtonPanel(uniqueName, currentStatus)) activeTabMapping.remove(oldTitle) activeTabMapping[uniqueName] = panel @@ -271,7 +292,7 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), } private fun renameAgentSession(tabIndex: Int) { - if (tabIndex <= 0) { + if (tabIndex < 0) { return } @@ -309,14 +330,8 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), fun resetCurrentlyActiveTabPanel() { tryFindActiveTabPanel().ifPresent { tabPanel -> - val oldSessionId = tabPanel.getSessionId() val oldDisplayName = tabPanel.getAgentSession().displayName - Disposer.dispose(tabPanel) - activeTabMapping.remove(getTitleAt(selectedIndex)) - removeTabAt(selectedIndex) - sessionStates.remove(oldSessionId) - - project.service().removeSession(oldSessionId) + closeTabAt(selectedIndex) val newSession = AgentSession( UUID.randomUUID().toString(), Conversation(), @@ -330,7 +345,6 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), private fun createTabButtonPanel( title: String, - closeable: Boolean, status: TabStatus = TabStatus.STOPPED ): JPanel { val titleLabel = JBLabel(title).apply { @@ -341,18 +355,16 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), val panel = JBUI.Panels.simplePanel(4, 0) .addToLeft(titleLabel) - if (closeable) { - val closeIcon = AllIcons.Actions.Close - val button = JButton(closeIcon).apply { - addActionListener(CloseActionListener(title)) - preferredSize = Dimension(closeIcon.iconWidth, closeIcon.iconHeight) - border = BorderFactory.createEmptyBorder() - isContentAreaFilled = false - toolTipText = "Close Agent" - rolloverIcon = AllIcons.Actions.CloseHovered - } - panel.addToRight(button) + val closeIcon = AllIcons.Actions.Close + val button = JButton(closeIcon).apply { + addActionListener(CloseActionListener(title)) + preferredSize = Dimension(closeIcon.iconWidth, closeIcon.iconHeight) + border = BorderFactory.createEmptyBorder() + isContentAreaFilled = false + toolTipText = "Close Agent" + rolloverIcon = AllIcons.Actions.CloseHovered } + panel.addToRight(button) return panel.andTransparent() } @@ -375,13 +387,7 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), override fun actionPerformed(evt: ActionEvent) { val tabIndex = indexOfTab(title) if (tabIndex >= 0) { - activeTabMapping[title]?.let { panel -> - sessionStates.remove(panel.getSessionId()) - project.service().removeSession(panel.getSessionId()) - Disposer.dispose(panel) - } - removeTabAt(tabIndex) - activeTabMapping.remove(title) + closeTabAt(tabIndex) } } } @@ -391,44 +397,46 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), init { add(createPopupMenuItem("Rename Title") { - if (selectedPopupTabIndex > 0) { + if (selectedPopupTabIndex >= 0) { renameAgentSession(selectedPopupTabIndex) } }) addSeparator() add(createPopupMenuItem("Close") { - if (selectedPopupTabIndex > 0) { - val title = getTitleAt(selectedPopupTabIndex) - activeTabMapping[title]?.let { panel -> - sessionStates.remove(panel.getSessionId()) - project.service().removeSession(panel.getSessionId()) - Disposer.dispose(panel) - } - removeTabAt(selectedPopupTabIndex) - activeTabMapping.remove(title) + if (selectedPopupTabIndex >= 0) { + closeTabAt(selectedPopupTabIndex) } }) add(createPopupMenuItem("Close Other Tabs") { + if (selectedPopupTabIndex < 0) { + return@createPopupMenuItem + } val selectedPopupTabTitle = getTitleAt(selectedPopupTabIndex) val tabPanel = activeTabMapping[selectedPopupTabTitle] - val keepSessionId = tabPanel?.getSessionId() - sessionStates.keys + if (tabPanel == null) { + return@createPopupMenuItem + } + val keepSessionId = tabPanel.getSessionId() + sessionStates.keys.toList() .filter { it != keepSessionId } .forEach { sessionStates.remove(it) } - activeTabMapping.values - .map { it.getSessionId() } - .filter { it != keepSessionId } - .forEach { project.service().removeSession(it) } + activeTabMapping.entries + .filter { it.key != selectedPopupTabTitle } + .forEach { entry -> + project.service().removeSession(entry.value.getSessionId()) + Disposer.dispose(entry.value) + } - clearAll() - tabPanel?.let { addNewTab(it) } + removeAll() + activeTabMapping.clear() + addNewTab(tabPanel) }) } override fun show(invoker: Component, x: Int, y: Int) { selectedPopupTabIndex = this@AgentToolWindowTabbedPane.getUI() .tabForCoordinate(this@AgentToolWindowTabbedPane, x, y) - if (selectedPopupTabIndex > 0) { + if (selectedPopupTabIndex >= 0) { super.show(invoker, x, y) } } @@ -443,4 +451,23 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(), override fun dispose() { clearAll() } + + private fun closeTabAt(tabIndex: Int) { + if (tabIndex !in 0 until tabCount) { + return + } + + val title = getTitleAt(tabIndex) + val panel = activeTabMapping.remove(title) + if (panel != null) { + sessionStates.remove(panel.getSessionId()) + project.service().removeSession(panel.getSessionId()) + Disposer.dispose(panel) + } + + removeTabAt(tabIndex) + if (activeTabMapping.isEmpty()) { + onAllTabsClosed() + } + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentToolWindowLandingPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentToolWindowLandingPanel.kt index 275d6778..b875f742 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentToolWindowLandingPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentToolWindowLandingPanel.kt @@ -43,7 +43,6 @@ import ee.carlrobert.codegpt.ui.UIUtil.createTextPane import kotlinx.coroutines.Dispatchers import ee.carlrobert.codegpt.util.coroutines.DisposableCoroutineScope import com.intellij.openapi.Disposable -import java.awt.BorderLayout import java.awt.Color import java.awt.Desktop import java.net.URI @@ -53,7 +52,7 @@ import javax.swing.Box import javax.swing.BoxLayout import javax.swing.JPanel -class AgentToolWindowLandingPanel(private val project: Project) : ResponseMessagePanel(), Disposable { +class AgentToolWindowLandingPanel(private val project: Project) : BorderLayoutPanel(), Disposable { companion object { private val logger = thisLogger() @@ -76,37 +75,51 @@ class AgentToolWindowLandingPanel(private val project: Project) : ResponseMessag } init { + isOpaque = false historyListPanel.onOpen = { thread -> openCheckpointThread(thread) } historyListPanel.onLoadPage = { query, offset, limit, onResult -> loadHistoryPage(query, offset, limit, onResult) } - addContent(buildContent()) + addToCenter(buildContent()) loadHistory() } private fun buildContent(): JPanel { - return BorderLayoutPanel().apply { - border = JBUI.Borders.empty(0) - add(topPanel(), BorderLayout.NORTH) - add(centerPanel(), BorderLayout.CENTER) + return JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + isOpaque = false + add(primaryMessagePanel()) + apiKeyPanel()?.let { add(it) } + } + } + + private fun primaryMessagePanel(): ResponseMessagePanel { + return ResponseMessagePanel().apply { + addContent( + BorderLayoutPanel().apply { + border = JBUI.Borders.empty(0) + addToTop(topPanel()) + addToCenter(centerPanel()) + } + ) } } private fun topPanel(): JPanel { return BorderLayoutPanel().apply { isOpaque = false - apiKeyPanel()?.let { addToTop(it) } addToCenter(createTextPane(welcomeMessage(), false)) } } - private fun apiKeyPanel(): JPanel? { + private fun apiKeyPanel(): ResponseMessagePanel? { val provider = ModelSettings.getInstance().getServiceForFeature(FeatureType.AGENT) if (provider != ServiceType.PROXYAI || CredentialsStore.isCredentialSet(CodeGptApiKey)) { return null } return ResponseMessagePanel().apply { + border = JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0) addContent( createTextPane( """ @@ -133,7 +146,6 @@ class AgentToolWindowLandingPanel(private val project: Project) : ResponseMessag } } ) - border = JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0) } } @@ -304,7 +316,7 @@ class AgentToolWindowLandingPanel(private val project: Project) : ResponseMessag private fun refresh() { removeAll() - addContent(buildContent()) + addToCenter(buildContent()) loadHistory() revalidate() repaint() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt index 413c9330..3f746d78 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt @@ -14,10 +14,8 @@ class ChatToolWindowListener : ToolWindowManagerListener { private fun requestFocusForTextArea(project: Project) { val contentManager = project.getService(ChatToolWindowContentManager::class.java) - contentManager.tryFindChatTabbedPane().ifPresent { tabbedPane -> - tabbedPane.tryFindActiveTabPanel().ifPresent { tabPanel -> - tabPanel.requestFocusForTextArea() - } + contentManager.tryFindChatToolWindowPanel().ifPresent { panel -> + panel.requestFocusForInput() } } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt index bff8291f..c128f124 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt @@ -1,12 +1,22 @@ package ee.carlrobert.codegpt.toolwindow.ui +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.ui.JBColor import com.intellij.ui.components.ActionLink import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.credentials.CredentialsStore +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.prompts.ChatActionsState +import ee.carlrobert.codegpt.settings.models.ModelSettings +import ee.carlrobert.codegpt.settings.service.FeatureType +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceConfigurable +import ee.carlrobert.codegpt.ui.UIUtil import ee.carlrobert.codegpt.ui.UIUtil.createTextPane +import ee.carlrobert.codegpt.util.ApplicationUtil import java.awt.BorderLayout import java.awt.Point import java.awt.event.ActionListener @@ -22,12 +32,57 @@ class ChatToolWindowLandingPanel(onAction: (LandingPanelAction, Point) -> Unit) private fun createContent(onAction: (LandingPanelAction, Point) -> Unit): JPanel { return BorderLayoutPanel().apply { - add(createTextPane(getWelcomeMessage(), false), BorderLayout.NORTH) + add( + BorderLayoutPanel().apply { + isOpaque = false + apiKeyPanel()?.let { addToTop(it) } + addToCenter(createTextPane(getWelcomeMessage(), false)) + }, + BorderLayout.NORTH + ) add(createActionsListPanel(onAction), BorderLayout.CENTER) add(createTextPane(getCautionMessage(), false), BorderLayout.SOUTH) } } + private fun apiKeyPanel(): JPanel? { + val provider = ModelSettings.getInstance().getServiceForFeature(FeatureType.CHAT) + if (provider != ServiceType.PROXYAI || CredentialsStore.isCredentialSet(CodeGptApiKey)) { + return null + } + + return BorderLayoutPanel().apply { + isOpaque = false + addToCenter( + createTextPane( + """ + +

+ It looks like you haven't configured your API key yet. Visit ProxyAI settings to do so. +

+

+ Don't have an account? Sign up to get started. +

+ + """.trimIndent(), + false + ) { event -> + if (event.eventType == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED && + event.description == "#OPEN_SETTINGS" + ) { + ShowSettingsUtil.getInstance().showSettingsDialog( + ApplicationUtil.findCurrentProject(), + CodeGPTServiceConfigurable::class.java + ) + } else { + UIUtil.handleHyperlinkClicked(event) + } + } + ) + border = JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0) + } + } + private fun createActionsListPanel(onAction: (LandingPanelAction, Point) -> Unit): JPanel { val listPanel = JPanel() listPanel.layout = BoxLayout(listPanel, BoxLayout.PAGE_AXIS) @@ -95,4 +150,3 @@ enum class LandingPanelAction( ChatActionsState.DEFAULT_EXPLAIN_PROMPT ) } - 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 80bea9f5..d0c86271 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.kt @@ -5,6 +5,7 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase import ee.carlrobert.codegpt.conversations.ConversationService import ee.carlrobert.codegpt.conversations.message.Message import org.assertj.core.api.Assertions.assertThat +import java.awt.event.ActionEvent class ChatToolWindowTabbedPaneTest : BasePlatformTestCase() { @@ -40,6 +41,29 @@ class ChatToolWindowTabbedPaneTest : BasePlatformTestCase() { assertThat(tabPanel!!.conversation.messages).isEmpty() } + fun testCanCloseFirstTabWhenMultipleTabsExist() { + val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable()) + tabbedPane.addNewTab(createNewTabPanel()) + tabbedPane.addNewTab(createNewTabPanel()) + + tabbedPane.CloseActionListener("Chat 1") + .actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close")) + + assertThat(tabbedPane.activeTabMapping.keys).containsExactly("Chat 2") + assertThat(tabbedPane.tabCount).isEqualTo(1) + } + + fun testCanCloseLastRemainingTab() { + val tabbedPane = ChatToolWindowTabbedPane(Disposer.newDisposable()) + tabbedPane.addNewTab(createNewTabPanel()) + + tabbedPane.CloseActionListener("Chat 1") + .actionPerformed(ActionEvent(tabbedPane, ActionEvent.ACTION_PERFORMED, "close")) + + assertThat(tabbedPane.activeTabMapping).isEmpty() + assertThat(tabbedPane.tabCount).isZero() + } + private fun createNewTabPanel(): ChatToolWindowTabPanel { return ChatToolWindowTabPanel( project,