From f3d11341c4b6d47a047e7f04dd46817e64a77bb1 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 27 Jul 2026 17:14:01 -0400 Subject: [PATCH] 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. --- .changeset/active-list-equal-rows.md | 5 +++ .../settings/agents/SkillsConfigurable.kt | 2 +- .../client/ui/list/ActiveListModel.kt | 2 +- .../client/ui/list/ActiveListRenderer.kt | 31 +++++++++++++++++-- .../kilocode/client/ui/list/ActiveListView.kt | 17 +++++++++- .../settings/base/SettingsListViewTest.kt | 31 +++++++++++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 .changeset/active-list-equal-rows.md diff --git a/.changeset/active-list-equal-rows.md b/.changeset/active-list-equal-rows.md new file mode 100644 index 0000000000..181ea56e84 --- /dev/null +++ b/.changeset/active-list-equal-rows.md @@ -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. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index abcf61de9e..61802cabe7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -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) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt index a71971efb8..e928ec10de 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt @@ -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, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt index 4e82905262..0a2ae95dad 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt @@ -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, private val cfg: ActiveListConfig = ActiveListConfig.Equal, ) : JPanel(BorderLayout()), ListCellRenderer { - 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, + 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) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt index 34b1b01777..2e07b3fb0b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt @@ -42,6 +42,7 @@ internal class ActiveListView( private val onCell: (String, String) -> Unit, ) : Stack(StackAxis.VERTICAL), Scrollable { private val model = CollectionListModel() + private val renderer = ActiveListRenderer(model, cfg) internal val list: JBList = object : JBList(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) { 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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index 126485ede0..e71cf8cdac 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -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()