diff --git a/plugins/agent-workbench/prompt/ui/src/AgentPromptPaletteView.kt b/plugins/agent-workbench/prompt/ui/src/AgentPromptPaletteView.kt index 048278f52c2f..c1823ba61691 100644 --- a/plugins/agent-workbench/prompt/ui/src/AgentPromptPaletteView.kt +++ b/plugins/agent-workbench/prompt/ui/src/AgentPromptPaletteView.kt @@ -28,6 +28,7 @@ import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.ui.EditorTextField import com.intellij.ui.WindowMoveListener import com.intellij.ui.components.ActionLink +import com.intellij.ui.components.DropDownLink import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane @@ -52,7 +53,6 @@ import java.awt.Container import java.awt.Cursor import java.awt.Dimension import java.awt.FlowLayout -import java.awt.Graphics import java.awt.event.ContainerAdapter import java.awt.event.ContainerEvent import java.awt.event.MouseAdapter @@ -317,12 +317,6 @@ internal class AgentPromptToolbarIconToggleAction( internal class HeaderActionLink(text: @Nls String) : ActionLink(text) { var onVisibilityChanged: (() -> Unit)? = null var onPresentationChanged: (() -> Unit)? = null - var trailingIcon: Icon? = null - set(value) { - field = value - revalidate() - repaint() - } override fun setVisible(aFlag: Boolean) { val visibilityChanged = isVisible != aFlag @@ -339,32 +333,14 @@ internal class HeaderActionLink(text: @Nls String) : ActionLink(text) { onPresentationChanged?.invoke() } } +} - override fun getPreferredSize(): Dimension { - return super.getPreferredSize().withTrailingIconWidth() - } - - override fun getMinimumSize(): Dimension { - return super.getMinimumSize().withTrailingIconWidth() - } - - override fun paintComponent(g: Graphics) { - super.paintComponent(g) - val icon = trailingIcon ?: return - val x = if (componentOrientation.isLeftToRight) { - width - insets.right - icon.iconWidth - } - else { - insets.left - } - val y = (height - icon.iconHeight) / 2 - icon.paintIcon(this, g, x, y) - } - - private fun Dimension.withTrailingIconWidth(): Dimension { - val icon = trailingIcon ?: return this - width += icon.iconWidth + iconTextGap - return this +internal class HeaderDropDownLink( + text: @Nls String, + private val onPerform: (HeaderDropDownLink) -> Unit, +) : DropDownLink(text, { error("HeaderDropDownLink handles popup display in performAction") }) { + override fun performAction() { + onPerform(this) } } @@ -383,23 +359,42 @@ internal class AgentPromptToolbarProfileAction( var onPresentationChanged: (() -> Unit)? = null @JvmField - val link: HeaderActionLink = HeaderActionLink(initialText).apply { + val link: HeaderDropDownLink = HeaderDropDownLink(initialText) { source -> + popupHandler.invoke(DataManager.getInstance().getDataContext(source), source) + }.apply { autoHideOnDisable = false withFont(JBUI.Fonts.smallFont().asPlain()) foreground = UIUtil.getContextHelpForeground() border = JBUI.Borders.empty() - setIcon(initialIcon, false) - trailingIcon = AllIcons.General.LinkDropTriangle setToolTipText(HtmlChunk.text(initialDescription)) accessibleContext.accessibleName = initialText accessibleContext.accessibleDescription = initialDescription - addActionListener { - popupHandler.invoke(DataManager.getInstance().getDataContext(this), this) - } } - val customComponent: JComponent - get() = link + @JvmField + val iconLabel: JLabel = JLabel(initialIcon).apply { + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + border = JBUI.Borders.emptyRight(4) + labelFor = link + setToolTipText(HtmlChunk.text(initialDescription)) + accessibleContext.accessibleName = initialText + accessibleContext.accessibleDescription = initialDescription + addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + if (e.button == MouseEvent.BUTTON1 && link.isEnabled) { + link.doClick() + } + } + }) + } + + private val component = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply { + isOpaque = false + add(iconLabel) + add(link) + } + + val customComponent: JComponent = component val textForTest: @Nls String get() = profileText @@ -430,19 +425,28 @@ internal class AgentPromptToolbarProfileAction( templatePresentation.description = description templatePresentation.icon = icon link.text = text + component.isVisible = visible + component.isEnabled = enabled link.isVisible = visible link.isEnabled = enabled - link.setIcon(icon, false) + iconLabel.icon = icon + iconLabel.isVisible = visible + iconLabel.isEnabled = enabled link.setToolTipText(HtmlChunk.text(description)) link.accessibleContext.accessibleName = text link.accessibleContext.accessibleDescription = description + iconLabel.setToolTipText(HtmlChunk.text(description)) + iconLabel.accessibleContext.accessibleName = text + iconLabel.accessibleContext.accessibleDescription = description + component.revalidate() + component.repaint() link.revalidate() link.repaint() onPresentationChanged?.invoke() } override fun createCustomComponent(presentation: Presentation, place: String): JComponent { - return link + return component } override fun update(e: AnActionEvent) { @@ -710,7 +714,7 @@ internal fun createAgentPromptPaletteView( } val generationSettingsActionsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0)).apply { isOpaque = false - add(launchProfileLink) + add(profileSelectorAction.customComponent) defaultProfileActionControl.component.border = JBUI.Borders.emptyLeft(if (isInlinePrompt) 6 else 8) add(defaultProfileActionControl.component) } diff --git a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt index 00f2d1f54001..c682d5f9662c 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt @@ -460,8 +460,10 @@ class AgentPromptPaletteViewStructureTest { val promptAreaInRoot = checkNotNull(findPromptArea(view.rootPanel, promptArea)) val profileActionComponent = view.profileAction.customComponent - assertThat(profileActionComponent).isSameAs(view.launchProfileLink) + assertThat(profileActionComponent).isNotSameAs(view.launchProfileLink) assertThat(SwingUtilities.isDescendingFrom(view.launchProfileLink, view.rootPanel)).isTrue() + assertThat(SwingUtilities.isDescendingFrom(view.launchProfileLink, profileActionComponent)).isTrue() + assertThat(SwingUtilities.isDescendingFrom(view.profileAction.iconLabel, profileActionComponent)).isTrue() assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.rootPanel)).isTrue() assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.rightHeaderPanel)).isFalse() assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.headerControls.toolbarComponent)).isFalse() @@ -500,14 +502,14 @@ class AgentPromptPaletteViewStructureTest { assertThat(view.generationSettingsPanel.isVisible).isTrue() assertThat(view.launchProfileLink.text).isEqualTo("Default") assertThat(view.profileAction.textForTest).isEqualTo("Default") - assertThat(view.launchProfileLink.icon).isNotNull() + assertThat(view.profileAction.iconLabel.icon).isNotNull() + assertThat(view.launchProfileLink.icon).isSameAs(view.addContextButton.icon) assertThat(view.launchProfileLink.font.isBold).isFalse() - assertThat((view.launchProfileLink as HeaderActionLink).trailingIcon).isSameAs(view.addContextButton.icon) assertThat(view.launchTuningSummaryLink.isVisible).isFalse() val launchTuningSummaryCenter = SwingUtilities.convertPoint( - view.launchProfileLink, - view.launchProfileLink.width / 2, - view.launchProfileLink.height / 2, + profileActionComponent, + profileActionComponent.width / 2, + profileActionComponent.height / 2, view.rootPanel, ) val topComponent = SwingUtilities.getDeepestComponentAt(view.rootPanel, launchTuningSummaryCenter.x, launchTuningSummaryCenter.y) @@ -515,6 +517,28 @@ class AgentPromptPaletteViewStructureTest { } } + @Test + fun launchSettingsProviderIconOpensSameControlAsDropdownLink() { + runInEdtAndWait { + val view = createAgentPromptPaletteView( + promptArea = EditorTextField(), + contextChipsPanel = JPanel(), + onExistingTaskSelected = {}, + ) + var popupAnchor: JComponent? = null + view.profileAction.setPopupHandler { _, anchor -> popupAnchor = anchor } + + triggerMousePressed(view.profileAction.iconLabel) + + assertThat(popupAnchor).isSameAs(view.launchProfileLink) + + popupAnchor = null + view.launchProfileLink.doClick() + + assertThat(popupAnchor).isSameAs(view.launchProfileLink) + } + } + @Test fun productionPromptFieldDoesNotReserveOverlaySpaceForGenerationSettingsControls() { runInEdtAndWait { diff --git a/plugins/agent-workbench/spec/actions/global-prompt-task-cost-profiles.spec.md b/plugins/agent-workbench/spec/actions/global-prompt-task-cost-profiles.spec.md index f6fb02731199..91cbd82e8cf5 100644 --- a/plugins/agent-workbench/spec/actions/global-prompt-task-cost-profiles.spec.md +++ b/plugins/agent-workbench/spec/actions/global-prompt-task-cost-profiles.spec.md @@ -36,7 +36,7 @@ Task-cost profiles make the global prompt ready to send while still letting user - This spec does not require renaming the existing persisted `activeLaunchProfileId` field; it may remain the stored default profile id for compatibility. ## Requirements -- The prompt composer exposes the launch-settings affordance in the bottom tray location specified by `global-prompt-composer.spec.md`. The affordance must show the selected provider icon, including the red YOLO/Brave badge for YOLO profiles, plus the compact selected profile name or generated profile name, and open one popup for profile choice, per-task tuning, and profile management. Default/save/update actions stay visible inline next to the launch-settings affordance. Built-in provider-backed profiles use compact mode labels, such as `Default`, `Full Auto`, `Skip Permissions`, or `Brave Mode`, to avoid repeating provider names. User profiles show their saved profile name, and unmatched edited controls show a generated compact name, such as `High` or `GPT-5 High`. +- The prompt composer exposes the launch-settings affordance in the bottom tray location specified by `global-prompt-composer.spec.md`. The affordance must show the selected provider icon, including the red YOLO/Brave badge for YOLO profiles, plus the compact selected profile name or generated profile name, and clicking either the icon or the text/dropdown link opens one popup for profile choice, per-task tuning, and profile management. Default/save/update actions stay visible inline next to the launch-settings affordance. Built-in provider-backed profiles use compact mode labels, such as `Default`, `Full Auto`, `Skip Permissions`, or `Brave Mode`, to avoid repeating provider names. User profiles show their saved profile name, and unmatched edited controls show a generated compact name, such as `High` or `GPT-5 High`. [@test] ../../prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt - The prompt must restore the stored default profile on open when that profile is still applicable. If the default is unavailable, the prompt may fall back to the provider-list default without writing a replacement default.