From b3f25a3f0307a47a502e1ffa09b55540bbf1d7e2 Mon Sep 17 00:00:00 2001 From: Sergey Ignatov Date: Sun, 28 Jun 2026 22:43:10 +0200 Subject: [PATCH] IJPL-248623 agent workbench: separate ACP agents in launch profile pickers Keep ACP launch profiles visually distinct from terminal providers in both new-thread launch profile menus and the welcome prompt launch settings popup. The split keeps normal providers first, places ACP catalog targets under their own section, and leaves YOLO profiles in the existing section. The tests cover both the action-group path and the custom popup-row path so the toolbar menu and welcome prompt stay aligned. GitOrigin-RevId: 216c3fdf17407f0bde90cf8abda350eb32f7982e --- .../messages/AgentPromptBundle.properties | 1 + ...AgentPromptGenerationSettingsController.kt | 62 ++++++++++----- .../AgentPromptProviderSelectorTest.kt | 76 +++++++++++++++++++ ...SessionsMainToolbarNewThreadActionsTest.kt | 57 ++++++++++++++ .../messages/AgentSessionsBundle.properties | 1 + .../AgentSessionLaunchProfileMenuActions.kt | 14 +++- 6 files changed, 189 insertions(+), 22 deletions(-) diff --git a/plugins/agent-workbench/prompt/ui/resources/messages/AgentPromptBundle.properties b/plugins/agent-workbench/prompt/ui/resources/messages/AgentPromptBundle.properties index 8b7ed5bb3bc8..d17b3a1c6844 100644 --- a/plugins/agent-workbench/prompt/ui/resources/messages/AgentPromptBundle.properties +++ b/plugins/agent-workbench/prompt/ui/resources/messages/AgentPromptBundle.properties @@ -48,6 +48,7 @@ popup.launch.settings.tooltip=Change launch profile, model, and reasoning for th popup.launch.settings.accessible.name.with.value=Launch settings: {0} popup.launch.settings.section.profile=Profile popup.profile.section.builtin=Built-in +popup.profile.section.acp=ACP Agents popup.profile.section.user=User Profiles popup.profile.group.model=Model popup.profile.group.effort=Effort diff --git a/plugins/agent-workbench/prompt/ui/src/AgentPromptGenerationSettingsController.kt b/plugins/agent-workbench/prompt/ui/src/AgentPromptGenerationSettingsController.kt index 5fb8ca163774..f88c3475ef42 100644 --- a/plugins/agent-workbench/prompt/ui/src/AgentPromptGenerationSettingsController.kt +++ b/plugins/agent-workbench/prompt/ui/src/AgentPromptGenerationSettingsController.kt @@ -591,20 +591,11 @@ internal class AgentPromptGenerationSettingsController( firstSeparatorText: @Nls String? = null, ): List { return buildList { - val profiles = launchableProfiles() - val standardProfiles = profiles.filter { profile -> profile.launchMode != AgentSessionLaunchMode.YOLO } - val yoloProfiles = profiles.filter { profile -> profile.launchMode == AgentSessionLaunchMode.YOLO } - standardProfiles.forEachIndexed { index, profile -> - add(createLaunchProfilePopupRow( - profile = profile, - separatorText = if (index == 0) firstSeparatorText else null, - )) - } - if (yoloProfiles.isNotEmpty()) { - yoloProfiles.forEachIndexed { index, profile -> + launchProfileSections(launchableProfiles(), firstSeparatorText).forEach { section -> + section.profiles.forEachIndexed { index, profile -> add(createLaunchProfilePopupRow( profile = profile, - separatorText = if (index == 0) AgentPromptBundle.message("popup.provider.section.auto") else null, + separatorText = section.title.takeIf { index == 0 }, )) } } @@ -835,22 +826,44 @@ internal class AgentPromptGenerationSettingsController( private fun createLaunchProfileActionGroup(): DefaultActionGroup { val group = DefaultActionGroup() - val profiles = launchableProfiles() - val standardProfiles = profiles.filter { profile -> profile.launchMode != AgentSessionLaunchMode.YOLO } - val yoloProfiles = profiles.filter { profile -> profile.launchMode == AgentSessionLaunchMode.YOLO } - standardProfiles.forEach { profile -> group.add(LaunchProfileAction(profile)) } - if (yoloProfiles.isNotEmpty()) { - if (standardProfiles.isNotEmpty()) { + var hasProfileActions = false + launchProfileSections(launchableProfiles(), firstSeparatorText = null).forEach { section -> + if (hasProfileActions) { group.add(Separator.getInstance()) } - group.add(Separator.create(AgentPromptBundle.message("popup.provider.section.auto"))) - yoloProfiles.forEach { profile -> group.add(LaunchProfileAction(profile)) } + section.title?.let { title -> group.add(Separator.create(title)) } + section.profiles.forEach { profile -> group.add(LaunchProfileAction(profile)) } + hasProfileActions = true } group.add(Separator.getInstance()) group.add(ManageProfilesAction()) return group } + private fun launchProfileSections( + profiles: List, + firstSeparatorText: @Nls String?, + ): List { + val standardProfiles = profiles.filter { profile -> + !profile.isAcpLaunchProfile() && profile.launchMode != AgentSessionLaunchMode.YOLO + } + val acpProfiles = profiles.filter { profile -> profile.isAcpLaunchProfile() } + val yoloProfiles = profiles.filter { profile -> + !profile.isAcpLaunchProfile() && profile.launchMode == AgentSessionLaunchMode.YOLO + } + return buildList { + if (standardProfiles.isNotEmpty()) { + add(LaunchProfileSection(firstSeparatorText, standardProfiles)) + } + if (acpProfiles.isNotEmpty()) { + add(LaunchProfileSection(AgentPromptBundle.message("popup.profile.section.acp"), acpProfiles)) + } + if (yoloProfiles.isNotEmpty()) { + add(LaunchProfileSection(AgentPromptBundle.message("popup.provider.section.auto"), yoloProfiles)) + } + } + } + private fun applyProfile(profile: AgentPromptLaunchProfile, persistForDraft: Boolean = true): Boolean { val providerEntry = findApplicableProviderEntry(profile) ?: return false val provider = providerEntry.bridge.provider @@ -1282,6 +1295,15 @@ internal class AgentPromptGenerationSettingsController( } +private data class LaunchProfileSection( + val title: @Nls String?, + val profiles: List, +) + +private fun AgentPromptLaunchProfile.isAcpLaunchProfile(): Boolean = providerId == ACP_PROVIDER_ID + +private const val ACP_PROVIDER_ID: String = "acp" + internal fun List.catalogReasoningEfforts(): Set? { val efforts = flatMapTo(LinkedHashSet()) { model -> model.supportedReasoningEfforts } return efforts.takeIf { it.isNotEmpty() } diff --git a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptProviderSelectorTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptProviderSelectorTest.kt index f104a1cc0ee0..a1d2cd68b4b5 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptProviderSelectorTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptProviderSelectorTest.kt @@ -25,6 +25,7 @@ import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProvid import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSource import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec import com.intellij.platform.ai.agent.sessions.core.providers.builtInLaunchProfileId +import com.intellij.platform.ai.agent.sessions.core.providers.builtInLaunchTargetProfileId import com.intellij.agent.workbench.sessions.providerItemMonochromeIconWithMode import com.intellij.agent.workbench.sessions.service.AgentSessionProviderAvailabilityService import com.intellij.agent.workbench.ui.AgentWorkbenchPopupRow @@ -611,6 +612,73 @@ class AgentPromptProviderSelectorTest { } } + @Test + fun launchSettingsPopupSeparatesAcpLaunchProfiles() { + runInEdtAndWait { + val acpProvider = AgentSessionProvider.from("acp") + val acpProfile = AgentPromptLaunchProfile( + id = builtInLaunchTargetProfileId(acpProvider, "acp.registry.stakpak", AgentSessionLaunchMode.STANDARD), + name = "Stakpak", + kind = AgentPromptLaunchProfileKind.BUILT_IN, + providerId = acpProvider.value, + launchTargetId = "acp.registry.stakpak", + ) + val launcher = TestPromptLauncherBridge( + AgentPromptLauncherBridge.ProviderPreferences( + launchProfiles = listOf(acpProfile), + ) + ) + val codexProvider = testProviderBridge( + provider = AgentSessionProvider.from("codex"), + promptOptions = emptyList(), + supportedLaunchModesOverride = setOf(AgentSessionLaunchMode.STANDARD, AgentSessionLaunchMode.YOLO), + ) + val acpBridge = testProviderBridge( + provider = acpProvider, + promptOptions = emptyList(), + supportsDefaultLaunchProfileOverride = false, + ) + val fixture = createSelectorFixture(listOf(codexProvider, acpBridge)) + fixture.selector.refresh() + val controller = AgentPromptGenerationSettingsController( + invocationData = testInvocationData(ProjectManager.getInstance().defaultProject), + providerSelector = fixture.selector, + generationSettingsPanel = fixture.view.generationSettingsPanel, + launchProfileLink = fixture.view.launchProfileLink, + modelSelectorLink = fixture.view.modelSelectorLink, + reasoningEffortLink = fixture.view.reasoningEffortLink, + modelCatalogScope = testScope(), + launcherProvider = { launcher }, + onDefaultSaved = { _ -> }, + ) + + controller.restoreLaunchProfiles(launcher.preferences) + + assertThat(actionEntries(controller.createLaunchProfileActionGroupForTest().getChildren(TestActionEvent.createTestEvent()))) + .containsExactly( + "action:Codex", + "separator:", + "separator:ACP Agents", + "action:Stakpak", + "separator:", + "separator:YOLO", + "action:Codex (Full Auto)", + "separator:", + "action:Manage Launch Profiles…", + ) + assertThat(popupRowEntries(controller.createLaunchSettingsPopupRowsForTest())).containsExactly( + "separator:Profile", + "row:Codex", + "separator:ACP Agents", + "row:Stakpak", + "separator:YOLO", + "row:Codex (Full Auto)", + "separator:", + "row:Manage Launch Profiles…", + ) + } + } + @Test fun launchProfilePopupRowsKeepDefaultMarkerSeparateFromCurrentSelection() { runInEdtAndWait { @@ -3512,6 +3580,12 @@ class AgentPromptProviderSelectorTest { } } + private fun actionEntries(actions: Array): List { + return actions.map { action -> + if (action is Separator) "separator:${action.text.orEmpty()}" else "action:${action.templatePresentation.text.orEmpty()}" + } + } + private fun popupEvent(action: AnAction): AnActionEvent { return AnActionEvent.createEvent( action, @@ -3559,6 +3633,7 @@ class AgentPromptProviderSelectorTest { supportsPlanReasoningEffortOverride: Boolean = false, availableGenerationModels: List = emptyList(), supportsGenerationModelSelection: Boolean = availableGenerationModels.isNotEmpty(), + supportsDefaultLaunchProfileOverride: Boolean = supportsPromptLaunch, availableGenerationModelsError: Throwable? = null, availableGenerationModelsResolver: suspend () -> List = { availableGenerationModels }, onListAvailableGenerationModels: () -> Unit = {}, @@ -3579,6 +3654,7 @@ class AgentPromptProviderSelectorTest { override val supportedReasoningEfforts: Set = supportedReasoningEffortsOverride override val supportsPlanReasoningEffort: Boolean = supportsPlanReasoningEffortOverride override val supportsGenerationModelSelection: Boolean = supportsGenerationModelSelection + override val supportsDefaultLaunchProfile: Boolean = supportsDefaultLaunchProfileOverride override val supportsPromptLaunch: Boolean = supportsPromptLaunch override val sessionSource: AgentSessionSource get() = error("Not required for this test") diff --git a/plugins/agent-workbench/sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt b/plugins/agent-workbench/sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt index 00623a00ceb3..49434474a94b 100644 --- a/plugins/agent-workbench/sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt +++ b/plugins/agent-workbench/sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt @@ -1030,6 +1030,63 @@ class AgentSessionsMainToolbarNewThreadActionsTest { ) } + @Test + fun pickerGroupSeparatesAcpProfilesFromTerminalProfiles() { + val context = newThreadContext(path = "/tmp/repo-direct") + val codexBridge = TestAgentSessionProviderDescriptor( + provider = AgentSessionProvider.from("codex"), + supportedModes = setOf(AgentSessionLaunchMode.STANDARD, AgentSessionLaunchMode.YOLO), + cliAvailable = true, + yoloSessionLabelKey = "toolwindow.action.new.session.codex.yolo", + ) + val acpBridge = TestAgentSessionProviderDescriptor( + provider = AgentSessionProvider.from("acp"), + supportedModes = setOf(AgentSessionLaunchMode.STANDARD), + cliAvailable = true, + supportsPromptLaunch = false, + newSessionLabelKeyOverride = "toolwindow.action.new.session.acp", + ) + val acpProfile = AgentPromptLaunchProfile( + id = "builtin:acp:target:mistral-vibe:standard", + name = "Mistral Vibe", + kind = AgentPromptLaunchProfileKind.BUILT_IN, + providerId = acpBridge.provider.value, + launchTargetId = "acp.registry.mistral-vibe", + ) + val action = AgentSessionsMainToolbarNewThreadAction( + resolveContext = { context }, + allBridges = { listOf(codexBridge, acpBridge) }, + userLaunchProfiles = { listOf(acpProfile) }, + createNewSession = { _, _, _, _ -> }, + ) + val event = TestActionEvent.createTestEvent(action) + + val children = action.actionGroup.getChildren(event) + val launchActionTexts = children + .filter { child -> child !is Separator } + .map { child -> child.templatePresentation.text } + .filter { text -> text != MANAGE_LAUNCH_PROFILES_TEXT } + assertThat(launchActionTexts).containsExactly( + AgentSessionsBundle.message("toolwindow.action.new.session.codex"), + "Mistral Vibe", + AgentSessionsBundle.message("toolwindow.action.new.session.codex.yolo"), + ) + assertThat(children.filterIsInstance()).hasSizeGreaterThanOrEqualTo(2) + + val rows = action.createProfilePickerRowsForTest(event) + val launchRows = rows.filter { row -> row.text != MANAGE_LAUNCH_PROFILES_TEXT } + assertThat(launchRows.map { row -> row.text }).containsExactly( + AgentSessionsBundle.message("toolwindow.action.new.session.codex"), + "Mistral Vibe", + AgentSessionsBundle.message("toolwindow.action.new.session.codex.yolo"), + ) + assertThat(launchRows.single { row -> row.text == "Mistral Vibe" }.separatorText) + .isEqualTo(AgentSessionsBundle.message("toolwindow.action.new.session.section.acp")) + val yoloRow = launchRows.single { row -> row.text == AgentSessionsBundle.message("toolwindow.action.new.session.codex.yolo") } + assertThat(yoloRow.separatorText) + .isEqualTo(AgentSessionsBundle.message("toolwindow.action.new.session.section.auto")) + } + @Test fun pickerGroupReturnsCandidateSubGroupsForCandidatesTarget() { val context = newThreadContext( diff --git a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties index 2659cf6617b7..3143ba43027c 100644 --- a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties +++ b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties @@ -244,6 +244,7 @@ toolwindow.action.new.session.acp=New ACP Thread toolwindow.action.new.session.terminal.description=Start a new terminal session toolwindow.action.new.session.terminal.title=Terminal toolwindow.action.new.session.section.normal=Normal +toolwindow.action.new.session.section.acp=ACP Agents toolwindow.action.new.session.section.auto=YOLO toolwindow.action.new.session.unavailable={0} CLI not found toolwindow.thread.fallback.title=Thread {0} diff --git a/plugins/agent-workbench/sessions/src/AgentSessionLaunchProfileMenuActions.kt b/plugins/agent-workbench/sessions/src/AgentSessionLaunchProfileMenuActions.kt index 84e0a015c30d..e21c17c950cd 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionLaunchProfileMenuActions.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionLaunchProfileMenuActions.kt @@ -273,12 +273,20 @@ private fun forEachLaunchProfileSection( profiles: List, handleSection: (@Nls String?, List) -> Unit, ) { - val standardProfiles = profiles.filter { profileItem -> profileItem.profile.launchMode != AgentSessionLaunchMode.YOLO } - val yoloProfiles = profiles.filter { profileItem -> profileItem.profile.launchMode == AgentSessionLaunchMode.YOLO } + val acpProfiles = profiles.filter(AgentSessionLaunchProfileMenuItem::isAcpProfile) + val standardProfiles = profiles.filter { profileItem -> + !profileItem.isAcpProfile() && profileItem.profile.launchMode != AgentSessionLaunchMode.YOLO + } + val yoloProfiles = profiles.filter { profileItem -> + !profileItem.isAcpProfile() && profileItem.profile.launchMode == AgentSessionLaunchMode.YOLO + } handleSection(null, standardProfiles) + handleSection(AgentSessionsBundle.message("toolwindow.action.new.session.section.acp"), acpProfiles) handleSection(AgentSessionsBundle.message("toolwindow.action.new.session.section.auto"), yoloProfiles) } +private fun AgentSessionLaunchProfileMenuItem.isAcpProfile(): Boolean = profile.providerId == ACP_PROVIDER_ID + private fun createLaunchProfileMenuRow( path: String, project: Project, @@ -349,3 +357,5 @@ private class LaunchProfileMenuAction( override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT } + +private const val ACP_PROVIDER_ID: String = "acp"