diff --git a/.changeset/crisp-cactus-jetbrains-settings.md b/.changeset/crisp-cactus-jetbrains-settings.md new file mode 100644 index 00000000000..c591667b1e1 --- /dev/null +++ b/.changeset/crisp-cactus-jetbrains-settings.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve JetBrains model settings dropdown direction, logged-out profile branding, and CLI menu labeling. 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 60b925da60a..7d50410baeb 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 @@ -5,7 +5,6 @@ import ai.kilocode.client.ui.PickerButton import com.intellij.icons.AllIcons import com.intellij.openapi.ui.popup.JBPopupFactory 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.util.ui.EmptyIcon @@ -17,8 +16,8 @@ import javax.swing.Icon /** * 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 + * Shows the selected item's display text with a down-arrow. On click, + * opens a list popup below the picker. Disabled (greyed out, not * clickable) when no items are loaded. */ class ReasoningPicker : PickerButton() { @@ -76,7 +75,7 @@ class ReasoningPicker : PickerButton() { } isVisible = true val display = selected?.display ?: items.firstOrNull()?.display ?: "" - text = "$display ▴" + text = "$display ▾" isEnabled = true cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } @@ -96,7 +95,7 @@ class ReasoningPicker : PickerButton() { } val popup: ListPopup = JBPopupFactory.getInstance().createListPopup(step) - popup.show(PopupShowOptions.aboveComponent(this)) + popup.showUnderneathOf(this) } private fun icon(item: Item): Icon = if (item.id == selected?.id) checked else empty 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 7ab82397889..00cc72ec6b9 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 @@ -5,7 +5,6 @@ import ai.kilocode.client.ui.PickerButton import ai.kilocode.rpc.dto.ModelSelectionDto import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.PopupShowOptions import com.intellij.openapi.ui.popup.util.PopupUtil import com.intellij.ui.CollectionListModel import com.intellij.ui.DocumentAdapter @@ -115,7 +114,7 @@ class ModelPicker : PickerButton() { return } val display = selected?.display - text = if (display == null && allowEmpty) "$emptyText ▴" else "${ModelText.sanitize(display ?: items.first().display)} ▴" + text = if (display == null && allowEmpty) "$emptyText ▾" else "${ModelText.sanitize(display ?: items.first().display)} ▾" isEnabled = true cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } @@ -297,7 +296,7 @@ class ModelPicker : PickerButton() { .setMovable(false) .createPopup() - popup.show(PopupShowOptions.aboveComponent(this)) + popup.showUnderneathOf(this) SwingUtilities.invokeLater { search.textEditor.requestFocusInWindow() search.selectText() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt index aa74f1c5d31..c3019b12721 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt @@ -3,17 +3,20 @@ package ai.kilocode.client.settings.profile import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.ui.RoundedContentPanel import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.HAlign +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.layout.VAlign +import ai.kilocode.client.ui.layout.align import ai.kilocode.log.KiloLog import ai.kilocode.rpc.dto.ProfileDto import com.intellij.icons.AllIcons import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.IconLoader import com.intellij.ui.RelativeFont import com.intellij.ui.components.JBLabel import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel -import java.awt.GridBagConstraints -import java.awt.GridBagLayout import java.awt.KeyboardFocusManager import java.awt.event.FocusEvent import java.awt.event.FocusListener @@ -22,6 +25,7 @@ import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel import javax.swing.SwingConstants +import java.awt.BorderLayout /** * Retained logged-in UI. Labels, combo box, and buttons are built once and @@ -43,6 +47,10 @@ internal class LoggedInProfileUi( foreground = UiStyle.Colors.weak() setCopyable(true) } + private val logoLabel = JBLabel(IconLoader.getIcon("/icons/kilo-profile.svg", LoggedInProfileUi::class.java)).apply { + name = "kilo.profile.logo.loggedIn" + accessibleContext.accessibleName = KiloBundle.message("settings.kilo.displayName") + } private val titleLabel = JBLabel(KiloBundle.message("profile.balance.title")).apply { foreground = UiStyle.Colors.weak() @@ -65,16 +73,10 @@ internal class LoggedInProfileUi( private val balanceCard = RoundedContentPanel(UiStyle.Gap.pad(), UiStyle.Gap.xl()).apply { name = "kilo.profile.balanceCard" addToTop(titleLabel) - addToCenter(JPanel(GridBagLayout()).apply { - isOpaque = false - add(valueLabel, GridBagConstraints().apply { - gridx = 0; gridy = 0; anchor = GridBagConstraints.CENTER - }) - add(refreshBtn, GridBagConstraints().apply { - gridx = 0; gridy = 1; anchor = GridBagConstraints.CENTER - insets = JBUI.insetsTop(UiStyle.Gap.pad()) - }) - }) + addToCenter(Stack.vertical(UiStyle.Gap.pad()) + .next(valueLabel) + .next(refreshBtn) + .align(HAlign.CENTER, VAlign.CENTER)) } private val comboModel = DefaultComboBoxModel() @@ -85,29 +87,23 @@ internal class LoggedInProfileUi( val logoutBtn = JButton(KiloBundle.message("profile.action.logout")) .also { it.addActionListener { logout() } } - private val actionRow = JPanel(GridBagLayout()).apply { - add(dashboardBtn, GridBagConstraints().apply { - gridx = 0; gridy = 0; anchor = GridBagConstraints.WEST - }) - add(logoutBtn, GridBagConstraints().apply { - gridx = 1; gridy = 0; anchor = GridBagConstraints.WEST - insets = JBUI.insetsLeft(UiStyle.Gap.md()) - }) + private val actionRow = Stack.horizontal(UiStyle.Gap.md()) + .next(dashboardBtn) + .next(logoutBtn) + + private val header = JPanel(BorderLayout()).apply { + isOpaque = false + add(Stack.vertical(UiStyle.Gap.lg()) + .next(nameLabel) + .next(emailLabel), BorderLayout.CENTER) + add(logoLabel, BorderLayout.EAST) } - private val rows: List = listOf(nameLabel, emailLabel, combo, balanceCard, actionRow) - - private val content = JPanel(GridBagLayout()).apply { - val gap = UiStyle.Gap.lg() - rows.forEachIndexed { i, comp -> - add(comp, GridBagConstraints().apply { - gridx = 0; gridy = i - weightx = 1.0 - fill = GridBagConstraints.HORIZONTAL - anchor = GridBagConstraints.WEST - insets = if (i == 0) JBUI.emptyInsets() else JBUI.insetsTop(gap) - }) - } + private val content = Stack.vertical(UiStyle.Gap.lg()).apply { + next(header) + next(combo) + next(balanceCard) + next(actionRow.align(HAlign.CENTER, VAlign.CENTER)) } private var applying = false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt index 80f870dbbf8..03326e588a7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt @@ -7,6 +7,7 @@ import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.KiloAppStatusDto import com.intellij.icons.AllIcons import com.intellij.openapi.ide.CopyPasteManager +import com.intellij.openapi.util.IconLoader import com.intellij.openapi.ui.popup.Balloon import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.SimpleColoredComponent @@ -124,6 +125,12 @@ internal class LoggedOutProfileUi( foreground = UiStyle.Colors.weak() } + private val logoLabel = JBLabel(IconLoader.getIcon("/icons/kilo-profile.svg", LoggedOutProfileUi::class.java)).apply { + name = "kilo.profile.logo.loggedOut" + horizontalAlignment = SwingConstants.CENTER + accessibleContext.accessibleName = KiloBundle.message("settings.kilo.displayName") + } + private val errLabel = JBLabel().apply { foreground = UiStyle.Colors.errorLabelForeground() horizontalAlignment = SwingConstants.CENTER @@ -179,11 +186,12 @@ internal class LoggedOutProfileUi( private fun emptyCard(): JPanel { val p = padded() + p.add(logoLabel, gbc(0).centered()) p.add(JBLabel(KiloBundle.message("profile.notLoggedIn")).apply { foreground = UiStyle.Colors.weak() horizontalAlignment = SwingConstants.CENTER - }, gbc(0)) - p.add(loginBtn, gbc(1, UiStyle.Gap.sm()).centered()) + }, gbc(1, UiStyle.Gap.pad())) + p.add(loginBtn, gbc(2, UiStyle.Gap.sm()).centered()) return p } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile.svg new file mode 100644 index 00000000000..3c44cd2f564 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile_dark.svg new file mode 100644 index 00000000000..1b0773f2641 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/kilo-profile_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index c54efa1e297..4310d07140f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -70,7 +70,7 @@ - + diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index f99429a1679..d6e5b07c313 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -26,6 +26,14 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertTrue("Reinstall should force-enable recovery action", event.presentation.isEnabled) } + fun `test cli group has visible menu text`() { + val xml = requireNotNull(javaClass.classLoader.getResourceAsStream("kilo.jetbrains.frontend.xml")) + .bufferedReader() + .use { it.readText() } + + assertTrue(xml.contains("")) + } + private fun event(action: AnAction): AnActionEvent { val presentation = Presentation().apply { copyFrom(action.templatePresentation) } presentation.isEnabled = false diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index a3c8a93d072..82d843f9f8e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -96,6 +96,7 @@ class PromptPanelTest : BasePlatformTestCase() { assertTrue(panel.reasoning.isVisible) assertEquals("high", panel.reasoning.selectedForTest()?.id) + assertEquals("High ▾", panel.reasoning.text) } fun `test reasoning picker aligns unchecked rows`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index 1d6980cbe8a..66e0897e61f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -202,15 +202,18 @@ class ModelPickerTest : BasePlatformTestCase() { picker.setItems(listOf(item("a", "A", "openai", "OpenAI"))) assertEquals("openai/a", picker.selectionKeyForTest()) + assertEquals("A ▾", picker.text) } fun `test allowEmpty keeps empty selection`() { val picker = ModelPicker() picker.allowEmpty = true + picker.emptyText = "Not set" picker.setItems(listOf(item("a", "A", "openai", "OpenAI"))) assertNull(picker.selectionKeyForTest()) + assertEquals("Not set ▾", picker.text) } fun `test provider qualified default selects duplicate model id from correct provider`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt index ad553ea603a..898b54e61c6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt @@ -161,6 +161,14 @@ class UserProfileConfigurableTest : BasePlatformTestCase() { panel.setSize(800, 600) layout(panel) + val logo = labelsByName(panel, "kilo.profile.logo.loggedIn").single() + val name = labels(panel).first { it.text == "Alice" } + val logoLoc = SwingUtilities.convertPoint(logo.parent, logo.location, panel) + val nameLoc = SwingUtilities.convertPoint(name.parent, name.location, panel) + assertNotNull(logo.icon) + assertTrue(logo.icon.iconWidth >= 64) + assertTrue(logoLoc.x > nameLoc.x) + val refresh = buttons(panel).first { it.text == "Refresh" } assertFalse(refresh.isContentAreaFilled) val card = refresh.parent @@ -208,6 +216,28 @@ class UserProfileConfigurableTest : BasePlatformTestCase() { } } + fun `test logged out profile shows kilo icon above login content`() { + edt { + panel.update(null, KiloAppStatusDto.READY) + panel.setSize(800, 600) + layout(panel) + + val logo = labelsByName(panel, "kilo.profile.logo.loggedOut").single() + val label = labels(panel).first { it.text == "Not logged in" } + val btn = buttons(panel).first { it.text == "Login with Kilo Code" } + val logoLoc = SwingUtilities.convertPoint(logo.parent, logo.location, panel) + val labelLoc = SwingUtilities.convertPoint(label.parent, label.location, panel) + val btnLoc = SwingUtilities.convertPoint(btn.parent, btn.location, panel) + + assertTrue(visible(logo)) + assertNotNull(logo.icon) + assertTrue(logo.icon.iconWidth >= 64) + assertTrue(logo.icon.iconHeight >= 64) + assertTrue(logoLoc.y < labelLoc.y) + assertTrue(labelLoc.y < btnLoc.y) + } + } + fun `test account update retains name label`() { val alice = ProfileDto(email = "alice@test.com", name = "Alice") val bob = ProfileDto(email = "bob@test.com", name = "Bob")