fix(jetbrains): align chat PR badge

This commit is contained in:
kirillk
2026-08-24 14:52:16 -04:00
parent d4f869d971
commit d24cedd170
6 changed files with 91 additions and 7 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Use regular PR title text and vertically center the PR badge in the JetBrains chat tool window.
@@ -21,7 +21,7 @@ internal class WorktreePrHeaderView(
openTerminal: () -> Unit = {},
openDiff: () -> Unit,
) : BorderLayoutPanel() {
private val core = PrHeaderView(openDiff)
private val core = PrHeaderView(openDiff = openDiff)
private val terminal = hoverTextButton(
ToolbarButtonAction(TerminalIcons.OpenTerminal_13x13, KiloBundle.message("worktree.session.terminal.action"), openTerminal),
tooltip = KiloBundle.message("worktree.session.terminal.tooltip"),
@@ -17,6 +17,7 @@ import com.intellij.openapi.actionSystem.DataSink
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.UiDataProvider
import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
@@ -43,8 +44,9 @@ internal class BranchDock(
openDiff: () -> Unit,
private val onMove: (() -> Unit)?,
private val onNewWorktree: (() -> Unit)? = null,
titleStyle: Int = SimpleTextAttributes.STYLE_PLAIN,
) : BorderLayoutPanel(), SessionEditorStyleTarget, UiDataProvider {
private val core = PrHeaderView(openDiff)
private val core = PrHeaderView(titleStyle = titleStyle, openDiff = openDiff)
private val changes = BranchChangesBadge(openDiff)
private val group = DefaultActionGroup().apply {
ActionManager.getInstance().getAction("Kilo.Chat.NewWorktree")?.let { add(it) }
@@ -66,7 +68,7 @@ internal class BranchDock(
toolbar.targetComponent = this
// Transparent so the toolbar shows the dock's prompt-matching background and tracks LaF.
toolbar.component.isOpaque = false
addToCenter(Stack.vertical().next(core).next(actionRow))
addToCenter(Stack.vertical().next(core).next(actionRow).align(HAlign.TRACK, VAlign.CENTER))
isVisible = false
sync()
}
@@ -77,7 +79,7 @@ internal class BranchDock(
super.updateUI()
border = JBUI.Borders.compound(
JBUI.Borders.customLineTop(JBUI.CurrentTheme.EditorTabs.borderColor()),
JBUI.Borders.empty(0, 0),
JBUI.Borders.empty(),
)
}
@@ -36,6 +36,7 @@ import java.awt.event.MouseEvent
* Stays non-opaque so the host owns background and borders.
*/
internal class PrHeaderView(
private val titleStyle: Int = SimpleTextAttributes.STYLE_BOLD,
openDiff: () -> Unit,
) : BorderLayoutPanel(), SessionEditorStyleTarget {
private val status = JBLabel()
@@ -165,10 +166,11 @@ internal class PrHeaderView(
val number = number ?: return
title.clear()
val body = body
val attrs = SimpleTextAttributes(titleStyle, UIUtil.getLabelForeground())
if (body == null) {
title.append(number, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, UIUtil.getLabelForeground()))
title.append(number, attrs)
} else {
title.append(body, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, UIUtil.getLabelForeground()))
title.append(body, attrs)
title.append(" $number", SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
}
@@ -184,7 +186,7 @@ internal class PrHeaderView(
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
changes.applyStyle(style)
// Re-render the title so its bold foreground follows the theme, then repaint.
// Re-render the title so its foreground follows the theme, then repaint.
renderTitle()
changed()
}
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.ui.header
import ai.kilocode.client.actions.ChatMoveToWorktreeAction
import ai.kilocode.client.actions.ChatNewWorktreeAction
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.ui.FilledBadgeIcon
import ai.kilocode.client.util.edtWait
import ai.kilocode.rpc.dto.BranchStatusDto
import ai.kilocode.rpc.dto.DiffFileDto
@@ -15,6 +16,13 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLabel
import java.awt.Component
import java.awt.Container
import java.awt.Point
import javax.swing.SwingUtilities
@Suppress("UnstableApiUsage")
class BranchDockTest : BasePlatformTestCase() {
@@ -71,6 +79,27 @@ class BranchDockTest : BasePlatformTestCase() {
assertTrue(edt { dock.isVisible })
}
fun `test PR title uses normal text in the tool window dock`() {
val dock = dock()
edt { dock.setBranch(prBranch()) }
val title = edt { components(dock).filterIsInstance<SimpleColoredComponent>().single() }
assertEquals(SimpleTextAttributes.STYLE_PLAIN, edt { firstAttrs(title).style })
}
fun `test PR state badge is vertically centered in the reserved dock row`() {
val dock = dock()
edt {
dock.setBranch(prBranch())
dock.setSize(500, dock.preferredSize.height)
layout(dock)
}
val badge = edt { components(dock).filterIsInstance<JBLabel>().single { it.icon is FilledBadgeIcon } }
val center = edt { SwingUtilities.convertPoint(badge, Point(0, 0), dock).y + badge.height / 2 }
assertTrue(kotlin.math.abs(edt { dock.height / 2 } - center) <= 1)
}
// ---- active session ----
fun `test dock hidden while session is busy`() {
@@ -257,6 +286,35 @@ class BranchDockTest : BasePlatformTestCase() {
private fun dockWithNewWorktree(): BranchDock = edt { BranchDock(openDiff = {}, onMove = {}, onNewWorktree = {}) }
private fun prBranch() = BranchStatusDto(
branch = "feature-x",
worktree = true,
availability = GhAvailability.OK,
pr = WorktreePrDto("/repo", 7, GhState.OPEN, "https://pr/7", "Title"),
)
private fun firstAttrs(title: SimpleColoredComponent): SimpleTextAttributes {
val iter = title.iterator()
check(iter.hasNext()) { "missing title fragment" }
iter.next()
return iter.textAttributes
}
private fun layout(root: Component) {
root.doLayout()
if (root is Container) root.components.forEach(::layout)
}
private fun components(root: Component): List<Component> {
val out = mutableListOf<Component>()
fun visit(item: Component) {
out += item
if (item is Container) item.components.forEach { visit(it) }
}
visit(root)
return out
}
private fun update(action: AnAction, dock: BranchDock): Presentation {
val event = event(action, dock)
edt { ActionUtil.updateAction(action, event) }
@@ -10,6 +10,7 @@ import ai.kilocode.rpc.dto.WorktreePrDto
import ai.kilocode.rpc.dto.WorktreeStatsDto
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.UIUtil
import java.awt.Component
@@ -31,6 +32,15 @@ class PrHeaderViewTest : BasePlatformTestCase() {
assertEquals(Cursor.HAND_CURSOR, edt { title.cursor.type })
}
fun `test title style can be configured`() {
val view = edt { PrHeaderView(openDiff = {}, titleStyle = SimpleTextAttributes.STYLE_PLAIN) }
edt { view.update(files = 0, additions = 0, deletions = 0, pull = pull(GhState.OPEN), name = "feature-x") }
val title = edt { title(view) }
assertEquals(SimpleTextAttributes.STYLE_PLAIN, edt { firstAttrs(title).style })
}
fun `test no PR hides badge and title`() {
val view = edt { PrHeaderView {} }
@@ -112,6 +122,13 @@ class PrHeaderViewTest : BasePlatformTestCase() {
return out
}
private fun firstAttrs(title: SimpleColoredComponent): SimpleTextAttributes {
val iter = title.iterator()
check(iter.hasNext()) { "missing title fragment" }
iter.next()
return iter.textAttributes
}
private fun components(root: Component): List<Component> {
val out = mutableListOf<Component>()
fun visit(item: Component) {