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
This commit is contained in:
Sergey Ignatov
2026-07-09 18:59:27 +00:00
committed by intellij-monorepo-bot
parent 5b6c3cb096
commit b3f25a3f03
6 changed files with 189 additions and 22 deletions
@@ -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
@@ -591,20 +591,11 @@ internal class AgentPromptGenerationSettingsController(
firstSeparatorText: @Nls String? = null,
): List<AgentPromptPopupRow> {
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<AgentPromptLaunchProfile>,
firstSeparatorText: @Nls String?,
): List<LaunchProfileSection> {
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<AgentPromptLaunchProfile>,
)
private fun AgentPromptLaunchProfile.isAcpLaunchProfile(): Boolean = providerId == ACP_PROVIDER_ID
private const val ACP_PROVIDER_ID: String = "acp"
internal fun List<AgentPromptGenerationModel>.catalogReasoningEfforts(): Set<AgentPromptReasoningEffort>? {
val efforts = flatMapTo(LinkedHashSet()) { model -> model.supportedReasoningEfforts }
return efforts.takeIf { it.isNotEmpty() }
@@ -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<AnAction>): List<String> {
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<AgentPromptGenerationModel> = emptyList(),
supportsGenerationModelSelection: Boolean = availableGenerationModels.isNotEmpty(),
supportsDefaultLaunchProfileOverride: Boolean = supportsPromptLaunch,
availableGenerationModelsError: Throwable? = null,
availableGenerationModelsResolver: suspend () -> List<AgentPromptGenerationModel> = { availableGenerationModels },
onListAvailableGenerationModels: () -> Unit = {},
@@ -3579,6 +3654,7 @@ class AgentPromptProviderSelectorTest {
override val supportedReasoningEfforts: Set<AgentPromptReasoningEffort> = 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")
@@ -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<Separator>()).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(
@@ -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}
@@ -273,12 +273,20 @@ private fun forEachLaunchProfileSection(
profiles: List<AgentSessionLaunchProfileMenuItem>,
handleSection: (@Nls String?, List<AgentSessionLaunchProfileMenuItem>) -> 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"