fix(jetbrains): keep active list section rows compact

Use equal-height ActiveList rows by default while preserving variable-height provider settings. When sections are present, equalize only the row body so headers such as Today and Yesterday do not inflate every row in worktree session lists.
This commit is contained in:
kirillk
2026-07-27 17:14:01 -04:00
parent 89163e7e02
commit f3d11341c4
6 changed files with 82 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Use equal-height rows by default in Agent Manager and settings lists, keeping variable-height rows only for provider settings.
@@ -352,7 +352,7 @@ internal class SkillSourcesView(
) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) {
private val view = ActiveListView(
KiloBundle.message("settings.agentBehavior.skills.sources.empty"),
ActiveListConfig.Preferred.copy(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),
ActiveListConfig(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),
) { key, id ->
if (id == EDIT_CELL) edit(key)
}
@@ -19,7 +19,7 @@ internal data class ActiveListBadge(val text: String, val style: UiStyle.Badge.S
internal enum class ActiveListRowHeight { EQUAL, PREFERRED }
internal data class ActiveListConfig(
val height: ActiveListRowHeight,
val height: ActiveListRowHeight = ActiveListRowHeight.EQUAL,
val description: Boolean = true,
val descriptionIndent: Boolean = true,
val tooltip: Boolean = true,
@@ -15,6 +15,7 @@ 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.Dimension
import javax.swing.JList
import javax.swing.JPanel
import javax.swing.ListCellRenderer
@@ -24,7 +25,8 @@ internal class ActiveListRenderer(
private val model: CollectionListModel<ActiveListItem>,
private val cfg: ActiveListConfig = ActiveListConfig.Equal,
) : JPanel(BorderLayout()), ListCellRenderer<ActiveListItem> {
private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets())
private val insets = JBUI.CurrentTheme.Popup.separatorLabelInsets()
private val sep = GroupHeaderSeparator(insets)
private val top = JPanel(BorderLayout()).apply {
border = JBUI.Borders.empty()
add(sep, BorderLayout.NORTH)
@@ -55,6 +57,7 @@ internal class ActiveListRenderer(
add(actions, BorderLayout.EAST)
}
private val wrap = PickerRow()
private var bodyHeight: Int? = null
init {
isOpaque = true
@@ -81,8 +84,7 @@ internal class ActiveListRenderer(
val active = selected && (focused || list.hasFocus() || (list as? ActiveListActive)?.active() == true)
val fg = UIUtil.getListForeground(active, active || focused)
val weak = if (active) fg else UiStyle.Colors.weak()
val current = model.items.getOrNull(index)
val section = if (current === value) activeListSectionTitle(model.items, index) else null
val section = activeListSectionTitle(model.items, index)
background = list.background
top.background = list.background
@@ -90,6 +92,7 @@ internal class ActiveListRenderer(
sep.caption = section
sep.setHideLine(index == 0)
top.isVisible = section != null
top.setPreferredSize(section?.let { Dimension(0, sep.getFontMetrics(sep.font).height + insets.top + insets.bottom) })
title.clear()
title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
@@ -118,10 +121,32 @@ internal class ActiveListRenderer(
syncCells(value, active && list.isEnabled, list.isEnabled)
cellPane.isVisible = cells.isVisible
actions.isVisible = trail.isVisible || cellPane.isVisible
val height = bodyHeight
wrap.setPreferredSize(height?.let { Dimension(0, it) })
top.invalidate()
return this
}
fun setBodyHeight(height: Int?) {
if (bodyHeight == height) return
bodyHeight = height
}
fun bodyPreferredHeight(
list: JList<out ActiveListItem>,
value: ActiveListItem,
index: Int,
selected: Boolean,
focused: Boolean,
): Int {
val fixed = bodyHeight
bodyHeight = null
getListCellRendererComponent(list, value, index, selected, focused)
val height = wrap.preferredSize.height
bodyHeight = fixed
return height
}
private fun syncBadges(item: ActiveListItem) {
val items = item.badges
while (badges.componentCount > items.size) badges.remove(badges.componentCount - 1)
@@ -42,6 +42,7 @@ internal class ActiveListView(
private val onCell: (String, String) -> Unit,
) : Stack(StackAxis.VERTICAL), Scrollable {
private val model = CollectionListModel<ActiveListItem>()
private val renderer = ActiveListRenderer(model, cfg)
internal val list: JBList<ActiveListItem> = object : JBList<ActiveListItem>(model), ActiveListActive {
override fun active(): Boolean = popups > 0
@@ -82,7 +83,7 @@ internal class ActiveListView(
init {
list.putClientProperty(AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED, true)
list.cellRenderer = ActiveListRenderer(model, cfg)
list.cellRenderer = renderer
list.registerKeyboardAction(
{ open(enter()) },
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
@@ -314,12 +315,26 @@ internal class ActiveListView(
@RequiresEdt
private fun syncCellHeight(rows: List<ActiveListItem>) {
checkEdt()
renderer.setBodyHeight(null)
if (cfg.height == ActiveListRowHeight.PREFERRED) {
if (list.fixedCellHeight == -1) return
list.fixedCellHeight = -1
list.revalidate()
return
}
if (rows.any { it.section != null }) {
val height = rows.indices.maxOfOrNull { idx ->
renderer.bodyPreferredHeight(list, rows[idx], idx, true, true)
}
renderer.setBodyHeight(height)
if (list.fixedCellHeight == -1) {
list.revalidate()
return
}
list.fixedCellHeight = -1
list.revalidate()
return
}
val height = rows.indices.maxOfOrNull { idx ->
list.cellRenderer.getListCellRendererComponent(list, rows[idx], idx, true, true).preferredSize.height
} ?: -1
@@ -11,6 +11,7 @@ import ai.kilocode.client.ui.list.ActiveListCell
import ai.kilocode.client.ui.list.ActiveListConfig
import ai.kilocode.client.ui.list.ActiveListItem
import ai.kilocode.client.ui.list.ActiveListRenderer
import ai.kilocode.client.ui.list.ActiveListRowHeight
import ai.kilocode.client.ui.list.ActiveListSelection
import ai.kilocode.client.ui.list.ActiveListView
import ai.kilocode.client.ui.list.activeListCellAt
@@ -84,6 +85,30 @@ class SettingsListViewTest : BasePlatformTestCase() {
}
}
fun `test active list config defaults to equal row height`() {
assertEquals(ActiveListRowHeight.EQUAL, ActiveListConfig().height)
assertEquals(ActiveListRowHeight.PREFERRED, ActiveListConfig.Preferred.height)
}
fun `test equal rows keep section headers out of body height`() {
edt {
val view = ActiveListView("Empty") { _, _ -> }
view.update(listOf(
sectionItem("first", "First", "Today"),
sectionItem("second", "Second", "Today"),
sectionItem("third", "Third", "Yesterday"),
))
layout(view)
val first = view.list.getCellBounds(0, 0)
val second = view.list.getCellBounds(1, 1)
val third = view.list.getCellBounds(2, 2)
assertTrue("fixed=${view.list.fixedCellHeight} first=${first.height} second=${second.height} third=${third.height}", first.height > second.height)
assertTrue("fixed=${view.list.fixedCellHeight} first=${first.height} second=${second.height} third=${third.height}", third.height > second.height)
}
}
fun `test filtering recalculates equal row height for visible rows`() {
edt {
val view = ActiveListView("Empty") { _, _ -> }
@@ -501,6 +526,12 @@ class SettingsListViewTest : BasePlatformTestCase() {
override val cells = cells.toList()
}
private fun sectionItem(id: String, name: String, group: String) = object : ActiveListItem {
override val key = id
override val title = name
override val section = group
}
private fun layout(view: ActiveListView) {
view.list.size = Dimension(320, 160)
view.list.doLayout()