From a7bc6397b395bab31cc3ba86b74844b340c08a33 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 12 Feb 2026 09:19:06 +0100 Subject: [PATCH] IJPL-233558 IJ-MR-184958 IJ-MR-184993 IJ-MR-179029 IJ-MR-184126 IJ-MR-181153 IJ-MR-146078 IJ-MR-175479 IJ-MR-186058 feat(platform): hide project toolwindow via layout profile GitOrigin-RevId: f332445e972bb1ed26d8a3f7e4e5905379986059 --- .../ex/ProjectFrameCapabilitiesServiceTest.kt | 31 +++++--- .../ToolWindowLayoutProfileServiceTest.kt | 67 +++++++++++++++++ .../openapi/wm/impl/ToolWindowManagerImpl.kt | 13 +++- .../ToolWindowLayoutProfileProvider.kt | 71 +++++++++++++++++++ .../src/META-INF/PlatformExtensionPoints.xml | 1 + .../openapi/wm/ex/ProjectFrameCapabilities.kt | 11 ++- .../intellij.agent.workbench.sessions.xml | 1 + .../src/AgentWorkbenchFrameConstants.kt | 5 ++ ...rkbenchProjectFrameCapabilitiesProvider.kt | 7 +- ...orkbenchToolWindowLayoutProfileProvider.kt | 48 +++++++++++++ 10 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 platform/lang-impl/testSources/com/intellij/toolWindow/ToolWindowLayoutProfileServiceTest.kt create mode 100644 platform/platform-impl/src/com/intellij/toolWindow/ToolWindowLayoutProfileProvider.kt create mode 100644 plugins/agent-workbench/sessions/src/AgentWorkbenchFrameConstants.kt create mode 100644 plugins/agent-workbench/sessions/src/AgentWorkbenchToolWindowLayoutProfileProvider.kt diff --git a/platform/lang-impl/testSources/com/intellij/openapi/wm/ex/ProjectFrameCapabilitiesServiceTest.kt b/platform/lang-impl/testSources/com/intellij/openapi/wm/ex/ProjectFrameCapabilitiesServiceTest.kt index eea541880085..593391e9548e 100644 --- a/platform/lang-impl/testSources/com/intellij/openapi/wm/ex/ProjectFrameCapabilitiesServiceTest.kt +++ b/platform/lang-impl/testSources/com/intellij/openapi/wm/ex/ProjectFrameCapabilitiesServiceTest.kt @@ -10,8 +10,7 @@ import com.intellij.testFramework.junit5.RunInEdt import com.intellij.testFramework.junit5.TestApplication import com.intellij.testFramework.junit5.TestDisposable import com.intellij.testFramework.junit5.fixture.projectFixture -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference @@ -26,7 +25,12 @@ class ProjectFrameCapabilitiesServiceTest { @Test fun uiPolicyUsesAggregatedCapabilitiesAndCapabilitiesAreCached() { - val uiPolicyRef = AtomicReference(ProjectFrameUiPolicy(projectPaneToActivateId = "pane-1")) + val uiPolicyRef = AtomicReference( + ProjectFrameUiPolicy( + projectPaneToActivateId = "pane-1", + toolWindowLayoutProfileId = "layout-profile-1", + ) + ) val capabilitiesComputationCount = AtomicInteger() val capabilitiesRef = AtomicReference(setOf(ProjectFrameCapability.WELCOME_EXPERIENCE)) ExtensionTestUtil.maskExtensions( @@ -56,16 +60,23 @@ class ProjectFrameCapabilitiesServiceTest { ) val service = ProjectFrameCapabilitiesService(service().coroutineScope) - assertEquals("pane-1", service.getUiPolicy(project)?.projectPaneToActivateId) + assertThat(service.getUiPolicy(project)?.projectPaneToActivateId).isEqualTo("pane-1") + assertThat(service.getUiPolicy(project)?.toolWindowLayoutProfileId).isEqualTo("layout-profile-1") - uiPolicyRef.set(ProjectFrameUiPolicy(projectPaneToActivateId = "pane-2")) - assertEquals("pane-2", service.getUiPolicy(project)?.projectPaneToActivateId) + uiPolicyRef.set( + ProjectFrameUiPolicy( + projectPaneToActivateId = "pane-2", + toolWindowLayoutProfileId = "layout-profile-2", + ) + ) + assertThat(service.getUiPolicy(project)?.projectPaneToActivateId).isEqualTo("pane-2") + assertThat(service.getUiPolicy(project)?.toolWindowLayoutProfileId).isEqualTo("layout-profile-2") - assertEquals(setOf(ProjectFrameCapability.WELCOME_EXPERIENCE), service.getAll(project)) - assertTrue(service.has(project, ProjectFrameCapability.WELCOME_EXPERIENCE)) + assertThat(service.getAll(project)).isEqualTo(setOf(ProjectFrameCapability.WELCOME_EXPERIENCE)) + assertThat(service.has(project, ProjectFrameCapability.WELCOME_EXPERIENCE)).isTrue() capabilitiesRef.set(emptySet()) - assertEquals(setOf(ProjectFrameCapability.WELCOME_EXPERIENCE), service.getAll(project)) - assertEquals(1, capabilitiesComputationCount.get()) + assertThat(service.getAll(project)).isEqualTo(setOf(ProjectFrameCapability.WELCOME_EXPERIENCE)) + assertThat(capabilitiesComputationCount.get()).isEqualTo(1) } } diff --git a/platform/lang-impl/testSources/com/intellij/toolWindow/ToolWindowLayoutProfileServiceTest.kt b/platform/lang-impl/testSources/com/intellij/toolWindow/ToolWindowLayoutProfileServiceTest.kt new file mode 100644 index 000000000000..af446435d8f2 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/toolWindow/ToolWindowLayoutProfileServiceTest.kt @@ -0,0 +1,67 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.toolWindow + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.impl.DesktopLayout +import com.intellij.testFramework.ExtensionTestUtil +import com.intellij.testFramework.junit5.RunInEdt +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable +import com.intellij.testFramework.junit5.fixture.projectFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +@TestApplication +@RunInEdt +class ToolWindowLayoutProfileServiceTest { + private val project by projectFixture() + + @TestDisposable + private lateinit var disposable: Disposable + + @Test + fun returnsLayoutFromFirstMatchingProvider() { + val firstLayout = DesktopLayout() + + ExtensionTestUtil.maskExtensions( + ToolWindowLayoutProfileProvider.EP_NAME, + listOf( + object : ToolWindowLayoutProfileProvider { + override fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? { + return firstLayout.takeIf { profileId == "dedicated" } + } + }, + object : ToolWindowLayoutProfileProvider { + override fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? { + return null + } + }, + ), + disposable, + ) + + val result = service().getLayout(project = project, profileId = "dedicated", isNewUi = true) + + assertThat(result).isSameAs(firstLayout) + } + + @Test + fun returnsNullWhenNoProviderMatchesProfileId() { + ExtensionTestUtil.maskExtensions( + ToolWindowLayoutProfileProvider.EP_NAME, + listOf( + object : ToolWindowLayoutProfileProvider { + override fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? { + return null + } + }, + ), + disposable, + ) + + val result = service().getLayout(project = project, profileId = "missing", isNewUi = true) + assertThat(result).isNull() + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.kt index 32ddd39559c6..c0219ea4082b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.kt @@ -87,6 +87,7 @@ import com.intellij.openapi.wm.ToolWindowType import com.intellij.openapi.wm.WINDOW_INFO_DEFAULT_TOOL_WINDOW_PANE_ID import com.intellij.openapi.wm.WindowInfo import com.intellij.openapi.wm.WindowManager +import com.intellij.openapi.wm.ex.ProjectFrameCapabilitiesService import com.intellij.openapi.wm.ex.ToolWindowEx import com.intellij.openapi.wm.ex.ToolWindowManagerEx import com.intellij.openapi.wm.ex.ToolWindowManagerListener @@ -101,6 +102,7 @@ import com.intellij.toolWindow.ToolWindowButtonManager import com.intellij.toolWindow.ToolWindowDefaultLayoutManager import com.intellij.toolWindow.ToolWindowEntry import com.intellij.toolWindow.ToolWindowEventSource +import com.intellij.toolWindow.ToolWindowLayoutProfileService import com.intellij.toolWindow.ToolWindowPane import com.intellij.toolWindow.ToolWindowPaneNewButtonManager import com.intellij.toolWindow.ToolWindowProperty @@ -424,7 +426,7 @@ open class ToolWindowManagerImpl @NonInjectable @TestOnly internal constructor( val updateHeadersRequests = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) coroutineScope.launch { updateHeadersRequests - .debounce(50) + .debounce(50.milliseconds) .collectLatest { for (project in getOpenedProjects()) { val toolWindowManager = project.serviceAsync() as ToolWindowManagerImpl @@ -774,7 +776,14 @@ open class ToolWindowManagerImpl @NonInjectable @TestOnly internal constructor( } private fun loadDefault() { - toolWindowSetInitializer.scheduleSetLayout(ToolWindowDefaultLayoutManager.getInstance().getLayoutCopy()) + val layout = getLayoutForProjectFrameProfile() ?: ToolWindowDefaultLayoutManager.getInstance().getLayoutCopy() + toolWindowSetInitializer.scheduleSetLayout(layout) + } + + private fun getLayoutForProjectFrameProfile(): DesktopLayout? { + val uiPolicy = service().getUiPolicy(project) ?: return null + val profileId = uiPolicy.toolWindowLayoutProfileId ?: return null + return service().getLayout(project = project, profileId = profileId, isNewUi = isNewUi) } @Deprecated("Use {@link ToolWindowManagerListener#TOPIC}", level = DeprecationLevel.ERROR) diff --git a/platform/platform-impl/src/com/intellij/toolWindow/ToolWindowLayoutProfileProvider.kt b/platform/platform-impl/src/com/intellij/toolWindow/ToolWindowLayoutProfileProvider.kt new file mode 100644 index 000000000000..bef667b4af68 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/toolWindow/ToolWindowLayoutProfileProvider.kt @@ -0,0 +1,71 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.toolWindow + +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.impl.DesktopLayout +import kotlinx.coroutines.CancellationException +import org.jetbrains.annotations.ApiStatus.Internal +import org.jetbrains.annotations.VisibleForTesting + +/** + * Resolves a toolwindow layout for a specific project frame profile. + * + * The layout is used to seed a frame when no project-specific layout was persisted yet. + */ +@Internal +interface ToolWindowLayoutProfileProvider { + companion object { + @VisibleForTesting + val EP_NAME: ExtensionPointName = ExtensionPointName("com.intellij.toolWindowLayoutProfileProvider") + } + + fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? +} + +@Service(Service.Level.APP) +@Internal +class ToolWindowLayoutProfileService { + fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? { + if (project.isDisposed) { + return null + } + + val providers = ToolWindowLayoutProfileProvider.EP_NAME.extensionList + if (providers.isEmpty()) { + return null + } + + var layout: DesktopLayout? = null + var layoutProvider: ToolWindowLayoutProfileProvider? = null + for (provider in providers) { + try { + val providerLayout = provider.getLayout(project = project, profileId = profileId, isNewUi = isNewUi) + if (providerLayout != null) { + if (layout == null) { + layout = providerLayout + layoutProvider = provider + } + else { + LOG.error( + "Multiple tool window layouts are provided for profile '$profileId'. " + + "Keeping ${layoutProvider?.javaClass?.name}, ignoring ${provider.javaClass.name}." + ) + } + } + } + catch (e: CancellationException) { + throw e + } + catch (e: Throwable) { + LOG.error("Tool window layout profile provider '${provider.javaClass.name}' failed", e) + } + } + + return layout + } +} + +private val LOG = logger() diff --git a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml index aa73823ae846..ee1c53b073ef 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml @@ -216,6 +216,7 @@ + diff --git a/platform/project-frame/src/com/intellij/openapi/wm/ex/ProjectFrameCapabilities.kt b/platform/project-frame/src/com/intellij/openapi/wm/ex/ProjectFrameCapabilities.kt index d0d7f106eb01..9237bbe2a1d8 100644 --- a/platform/project-frame/src/com/intellij/openapi/wm/ex/ProjectFrameCapabilities.kt +++ b/platform/project-frame/src/com/intellij/openapi/wm/ex/ProjectFrameCapabilities.kt @@ -54,9 +54,18 @@ data class ProjectFrameUiPolicy( /** Toolwindow ids to hide after startup activation. */ val toolWindowIdsToHideOnStartup: Set = emptySet(), + + /** + * Toolwindow layout profile id used to seed project frame layout on first open (when no + * project-specific toolwindow layout has been persisted yet). + */ + val toolWindowLayoutProfileId: String? = null, ) { fun isEmpty(): Boolean { - return projectPaneToActivateId == null && startupToolWindowIdToActivate == null && toolWindowIdsToHideOnStartup.isEmpty() + return projectPaneToActivateId == null && + startupToolWindowIdToActivate == null && + toolWindowIdsToHideOnStartup.isEmpty() && + toolWindowLayoutProfileId == null } } diff --git a/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml b/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml index 54fe54925ea7..9bc88fd09fbd 100644 --- a/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml +++ b/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml @@ -23,6 +23,7 @@ bundle="messages.AgentSessionsBundle" groupKey="advanced.settings.agent.workbench"/> + diff --git a/plugins/agent-workbench/sessions/src/AgentWorkbenchFrameConstants.kt b/plugins/agent-workbench/sessions/src/AgentWorkbenchFrameConstants.kt new file mode 100644 index 000000000000..e17f3aa156f4 --- /dev/null +++ b/plugins/agent-workbench/sessions/src/AgentWorkbenchFrameConstants.kt @@ -0,0 +1,5 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.agent.workbench.sessions + +internal const val AGENT_SESSIONS_TOOL_WINDOW_ID: String = "agent.workbench.sessions" +internal const val AGENT_WORKBENCH_DEDICATED_LAYOUT_PROFILE_ID: String = "agent.workbench.dedicated" diff --git a/plugins/agent-workbench/sessions/src/AgentWorkbenchProjectFrameCapabilitiesProvider.kt b/plugins/agent-workbench/sessions/src/AgentWorkbenchProjectFrameCapabilitiesProvider.kt index bb7870287f0c..fce72a550d00 100644 --- a/plugins/agent-workbench/sessions/src/AgentWorkbenchProjectFrameCapabilitiesProvider.kt +++ b/plugins/agent-workbench/sessions/src/AgentWorkbenchProjectFrameCapabilitiesProvider.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.wm.ToolWindowId import com.intellij.openapi.wm.ex.ProjectFrameCapabilitiesProvider import com.intellij.openapi.wm.ex.ProjectFrameCapability import com.intellij.openapi.wm.ex.ProjectFrameUiPolicy +import java.util.EnumSet internal class AgentWorkbenchProjectFrameCapabilitiesProvider : ProjectFrameCapabilitiesProvider { override fun getCapabilities(project: Project): Set { @@ -33,8 +34,10 @@ internal class AgentWorkbenchProjectFrameCapabilitiesProvider : ProjectFrameCapa } } -private val AGENT_WORKBENCH_FRAME_CAPABILITIES = setOf(ProjectFrameCapability.SUPPRESS_VCS_UI) +private val AGENT_WORKBENCH_FRAME_CAPABILITIES = EnumSet.of(ProjectFrameCapability.SUPPRESS_VCS_UI) + private val AGENT_WORKBENCH_FRAME_UI_POLICY = ProjectFrameUiPolicy( - startupToolWindowIdToActivate = "agent.workbench.sessions", + startupToolWindowIdToActivate = AGENT_SESSIONS_TOOL_WINDOW_ID, toolWindowIdsToHideOnStartup = setOf(ToolWindowId.PROJECT_VIEW), + toolWindowLayoutProfileId = AGENT_WORKBENCH_DEDICATED_LAYOUT_PROFILE_ID, ) diff --git a/plugins/agent-workbench/sessions/src/AgentWorkbenchToolWindowLayoutProfileProvider.kt b/plugins/agent-workbench/sessions/src/AgentWorkbenchToolWindowLayoutProfileProvider.kt new file mode 100644 index 000000000000..189cc5ff2e3e --- /dev/null +++ b/plugins/agent-workbench/sessions/src/AgentWorkbenchToolWindowLayoutProfileProvider.kt @@ -0,0 +1,48 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplacePutWithAssignment", "ReplaceGetOrSet") + +package com.intellij.agent.workbench.sessions + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowAnchor +import com.intellij.openapi.wm.ToolWindowId +import com.intellij.openapi.wm.WINDOW_INFO_DEFAULT_TOOL_WINDOW_PANE_ID +import com.intellij.openapi.wm.impl.DesktopLayout +import com.intellij.openapi.wm.impl.WindowInfoImpl +import com.intellij.toolWindow.ToolWindowDefaultLayoutManager +import com.intellij.toolWindow.ToolWindowLayoutProfileProvider +import com.intellij.ui.ExperimentalUI + +internal class AgentWorkbenchToolWindowLayoutProfileProvider : ToolWindowLayoutProfileProvider { + override fun getLayout(project: Project, profileId: String, isNewUi: Boolean): DesktopLayout? { + if (project.isDisposed) { + return null + } + if (profileId != AGENT_WORKBENCH_DEDICATED_LAYOUT_PROFILE_ID) { + return null + } + + val baseLayout = ToolWindowDefaultLayoutManager.getInstance().getLayoutCopy() + val infos = baseLayout.getInfos() + .asSequence() + .filter { (id, _) -> id != ToolWindowId.PROJECT_VIEW } + .associateTo(LinkedHashMap()) { (id, info) -> id to info.copy() } + + val sessionsInfo = infos.get(AGENT_SESSIONS_TOOL_WINDOW_ID) ?: WindowInfoImpl() + val paneId = WINDOW_INFO_DEFAULT_TOOL_WINDOW_PANE_ID + val nextOrderOnLeft = infos.values.asSequence() + .filter { it.toolWindowPaneId == paneId && it.anchor == ToolWindowAnchor.LEFT && it.order >= 0 } + .maxOfOrNull { it.order + 1 } ?: 0 + + sessionsInfo.id = AGENT_SESSIONS_TOOL_WINDOW_ID + sessionsInfo.toolWindowPaneId = paneId + sessionsInfo.anchor = ToolWindowAnchor.LEFT + sessionsInfo.order = nextOrderOnLeft + sessionsInfo.isVisible = true + sessionsInfo.isShowStripeButton = true + sessionsInfo.weight = 0.25f + infos.put(AGENT_SESSIONS_TOOL_WINDOW_ID, sessionsInfo) + + return DesktopLayout(infos, baseLayout.unifiedWeights.copy()) + } +}