From 7d2cb5ed6ed70620bf7a5bdc97ccd277798d1cdc Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 3 May 2026 00:01:43 -0400 Subject: [PATCH] fix(jetbrains): make prompt controls native --- .../kilocode/client/session/ui/PromptPanel.kt | 15 ++- .../client/session/ui/ReasoningPicker.kt | 6 +- .../client/session/ui/mode/ModePicker.kt | 4 +- .../client/session/ui/model/ModelPicker.kt | 4 +- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 121 +++++++++++++++++- 5 files changed, 128 insertions(+), 22 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PromptPanel.kt index 1023e31ee6c..2396c4d051f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PromptPanel.kt @@ -21,6 +21,7 @@ import javax.swing.Box import javax.swing.BoxLayout import javax.swing.Icon import javax.swing.JButton +import javax.swing.JComponent import javax.swing.ScrollPaneConstants /** @@ -90,13 +91,11 @@ class PromptPanel( } } - private val reset = JButton(AllIcons.Actions.Cancel).apply { - UiStyle.Buttons.icon(this) - isFocusPainted = false + private val reset = UiStyle.Buttons.HoverIcon().apply { + icon = AllIcons.Actions.Cancel toolTipText = KiloBundle.message("model.picker.reset") accessibleContext.accessibleName = KiloBundle.message("model.picker.reset") isVisible = false - preferredSize = JBUI.size(UiStyle.Size.BUTTON, UiStyle.Size.BUTTON) addActionListener { onReset() } } @@ -115,11 +114,11 @@ class PromptPanel( border = JBUI.Borders.emptyTop(UiStyle.Space.SM) } bar.add(mode) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(UiStyle.Gap.xs())) bar.add(model) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(UiStyle.Gap.xs())) bar.add(reasoning) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(UiStyle.Gap.xs())) bar.add(reset) bar.add(Box.createHorizontalGlue()) bar.add(button) @@ -152,6 +151,8 @@ class PromptPanel( internal fun resetVisibleForTest() = reset.isVisible + internal fun resetForTest(): JComponent = reset + override fun applyStyle(style: SessionStyle) { this.style = style editor.font = style.transcriptFont diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt index d4ff880d268..9970d33f2fb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.ui.popup.ListPopup import com.intellij.openapi.ui.popup.PopupShowOptions import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep -import com.intellij.ui.components.JBLabel import com.intellij.util.ui.EmptyIcon import java.awt.Cursor import java.awt.event.MouseAdapter @@ -15,13 +14,13 @@ import java.awt.event.MouseEvent import javax.swing.Icon /** - * Clickable label-style dropdown picker with a rounded outline. + * Clickable label-style dropdown picker with a native filled background. * * Shows the selected item's display text with an up-arrow. On click, * opens a list popup above the picker. Disabled (greyed out, not * clickable) when no items are loaded. */ -class ReasoningPicker : JBLabel() { +class ReasoningPicker : UiStyle.Pickers.Label() { private companion object { val checked: Icon = AllIcons.Actions.Checked @@ -38,7 +37,6 @@ class ReasoningPicker : JBLabel() { private var selected: Item? = null init { - border = UiStyle.Borders.picker() isEnabled = false isVisible = false text = " " diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt index 67bcdd6349c..306d19a8b5b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt @@ -3,13 +3,12 @@ package ai.kilocode.client.session.ui.mode import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupShowOptions -import com.intellij.ui.components.JBLabel import java.awt.Cursor import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.ListSelectionModel -class ModePicker : JBLabel() { +class ModePicker : UiStyle.Pickers.Label() { data class Item( val id: String, @@ -26,7 +25,6 @@ class ModePicker : JBLabel() { private var selected: Item? = null init { - border = UiStyle.Borders.picker() isEnabled = false text = " " diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index 1b9e249389a..4d930d36fbb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -15,7 +15,6 @@ import com.intellij.ui.ListUtil import com.intellij.ui.SearchTextField import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.ScrollingUtil -import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList import com.intellij.ui.popup.AbstractPopup import com.intellij.util.ui.JBUI @@ -46,7 +45,7 @@ private const val MODEL_PICKER_MAX_WIDTH = 760 private const val MODEL_PICKER_MAX_VISIBLE_ROWS = 10 private const val MODEL_PICKER_EMPTY_LIST_HEIGHT = 120 -class ModelPicker : JBLabel() { +class ModelPicker : UiStyle.Pickers.Label() { data class Item( val id: String, @@ -70,7 +69,6 @@ class ModelPicker : JBLabel() { private var selected: Item? = null init { - border = UiStyle.Borders.picker() isEnabled = false text = " " diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index d1116913a0a..8acff800e57 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -2,11 +2,17 @@ package ai.kilocode.client.ui import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.JBColor -import com.intellij.ui.RoundedLineBorder +import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.BorderLayout import java.awt.Color +import java.awt.Dimension +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.RenderingHints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import javax.swing.JButton import javax.swing.JComponent import javax.swing.UIManager @@ -69,6 +75,14 @@ object UiStyle { fun running(): Color = JBColor.namedColor("ProgressBar.foreground", UIUtil.getLabelForeground()) + fun picker(): Color = JBColor.lazy { + UIManager.getColor("ComboBoxButton.background") + ?: UIManager.getColor("ComboBox.nonEditableBackground") + ?: UIUtil.getPanelBackground() + } + + fun pickerHover(): Color = JBUI.CurrentTheme.ActionButton.hoverBackground() + internal fun contrast(base: Color, delta: Int): Color { val step = if (bright(base)) -delta else delta return Color( @@ -116,10 +130,7 @@ object UiStyle { fun warning(): Border = JBUI.Borders.customLine(Colors.warning(), 1) - fun picker(): Border = JBUI.Borders.compound( - RoundedLineBorder(Colors.line(), JBUI.scale(Space.MD)), - JBUI.Borders.empty(Space.XS, Space.LG), - )!! + fun picker(): Border = JBUI.Borders.empty(Space.XS, Space.LG) fun user(): Border = JBUI.Borders.compound( JBUI.Borders.customLineTop(Colors.line()), @@ -167,6 +178,8 @@ object UiStyle { } object Gap { + fun xs() = JBUI.scale(Space.XS) + fun inline() = JBUI.scale(Space.MD) fun regular() = JBUI.scale(Space.LG) @@ -183,6 +196,57 @@ object UiStyle { } object Buttons { + class HoverIcon : JButton() { + private var over = false + + init { + isFocusable = false + setRequestFocusEnabled(false) + isContentAreaFilled = false + isBorderPainted = false + isOpaque = false + border = JBUI.Borders.empty() + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun getPreferredSize(): Dimension = JBUI.size(Size.BUTTON, Size.BUTTON) + + override fun getMinimumSize(): Dimension = preferredSize + + override fun getMaximumSize(): Dimension = preferredSize + + override fun paintComponent(g: Graphics) { + if (isEnabled && over) paintHover(g) + super.paintComponent(g) + } + + private fun paintHover(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } + } + fun icon(button: JButton) { button.isFocusable = false button.setRequestFocusEnabled(false) @@ -193,6 +257,53 @@ object UiStyle { } } + object Pickers { + open class Label : JBLabel() { + private var over = false + + init { + border = Borders.picker() + background = Colors.picker() + // The custom rounded fill needs parent background around the corners. + isOpaque = false + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun updateUI() { + super.updateUI() + border = Borders.picker() + background = Colors.picker() + } + + override fun paintComponent(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = if (isEnabled && over) Colors.pickerHover() else Colors.picker() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + super.paintComponent(g) + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } + } + } + object Components { fun transparent(component: JComponent) { component.isOpaque = false