mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-01 17:03:12 +07:00
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:
committed by
intellij-monorepo-bot
parent
5b6c3cb096
commit
b3f25a3f03
@@ -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")
|
||||
|
||||
+57
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user