mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
fix(kilo-jetbrains): polish profile and picker UI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains model settings dropdown direction, logged-out profile branding, and CLI menu labeling.
|
||||
+4
-5
@@ -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
|
||||
|
||||
+2
-3
@@ -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()
|
||||
|
||||
+29
-33
@@ -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<String>()
|
||||
@@ -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<java.awt.Component> = 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
|
||||
|
||||
+10
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 0H0V512H512V0Z" fill="black"/>
|
||||
<path d="M322 377H377V421H307.857L278 391.143V322H322V377ZM421 307.857L391.143 278H322V322L377 322V377H421V307.857ZM234 278H190V322H234V278ZM91 391.143L120.857 421H234V377H135V278H91V391.143ZM371.172 189.999V120.856L341.315 90.9995H278V135H327.172V189.999H278V233.999H421V189.999H371.172ZM135 91H91V233.999H135V184.5H190V233.999H234V184.5L190 140.5H135V91ZM234 91H190V140.5H234V91Z" fill="#FAF74F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 558 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 0H0V512H512V0Z" fill="black"/>
|
||||
<path d="M512 512H0V0H512V512ZM322.783 322.784H278.261V392.747L308.472 422.958H378.435V378.437H322.782L322.783 322.784ZM422.957 308.474L392.746 278.263H322.783V322.784H378.435L378.435 378.437H422.957L422.957 308.474ZM233.739 278.263H189.217V322.784H233.739V278.263ZM89.0435 392.747L119.254 422.958H233.739V378.437H133.565V278.263H89.043L89.0435 392.747ZM372.538 189.217V119.254L342.327 89.0435H278.261V133.565H328.017V189.217H278.261V233.739H422.957V189.217H372.538ZM133.565 89.0435H89.0435V233.739H133.565V183.652H189.218V233.739H233.74V183.652L189.218 139.13H133.565V89.0435ZM233.739 89.0435H189.217L189.218 139.13H233.739V89.0435Z" fill="#FAF74F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
@@ -70,7 +70,7 @@
|
||||
<action id="Kilo.OpenSettings"
|
||||
class="ai.kilocode.client.actions.OpenSettingsAction"/>
|
||||
|
||||
<group id="Kilo.CliGroup" popup="true">
|
||||
<group id="Kilo.CliGroup" text="CLI" popup="true">
|
||||
<reference ref="Kilo.Restart"/>
|
||||
<reference ref="Kilo.Reinstall"/>
|
||||
</group>
|
||||
|
||||
+8
@@ -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("<group id=\"Kilo.CliGroup\" text=\"CLI\" popup=\"true\">"))
|
||||
}
|
||||
|
||||
private fun event(action: AnAction): AnActionEvent {
|
||||
val presentation = Presentation().apply { copyFrom(action.templatePresentation) }
|
||||
presentation.isEnabled = false
|
||||
|
||||
+1
@@ -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`() {
|
||||
|
||||
+3
@@ -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`() {
|
||||
|
||||
+30
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user