feat(jetbrains): refresh session UI colors and fix sticky header hover

Consolidate the session palette around three tunable roles: session
background (the whole backdrop), code-block background (code blocks,
tool/shell output, the user prompt bubble, and the prompt input), and
foreground (with hint derived from it). Containers stay transparent over
the backdrop and raised surfaces share the one code-block key, so the
transcript reads as a single dialog-like surface.

Apply the style correctly on first paint: re-run applyStyle on attach and
propagate it to the dynamically shown empty/recent view, instead of only
correcting colors after a theme switch.

Fix tool-row headers keeping a hover tint after the pointer left the row.
Hover was bound only to enumerated header elements, so leaving through an
unbound nested child (e.g. the file-name link) never delivered mouseExited.
Track hover across the whole header subtree, following children added later,
while keeping click/cursor binding on the enumerated set so a link click
still opens the file rather than toggling the card.
This commit is contained in:
kirillk
2026-08-11 13:49:06 -04:00
parent 907ed2b28d
commit 06fc620ba0
48 changed files with 291 additions and 162 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Refresh the session UI colors around three tunable roles — session background, code-block background (shared by code blocks, the prompt bubble, and the input), and foreground — and apply them correctly on first paint instead of only after switching themes.
@@ -234,6 +234,10 @@ class SessionUi(
override fun addNotify() {
if (disposed) return
super.addNotify()
// First realized paint: the constructor's applyStyle can run before the tool window is
// attached, when panel/editor colors are still provisional. Re-apply from the live scheme
// so the session is styled correctly without needing a theme toggle to trigger it.
applyStyle(SessionEditorStyle.current())
resumeOpen()
}
@@ -988,16 +992,18 @@ class SessionUi(
selection.applyStyle(style)
editorTheme = style.editorScheme
colorTheme = UIManager.getLookAndFeel()
background = style.editorBackground
root.background = style.editorBackground
root.content.background = style.editorBackground
sessionContent.background = style.editorBackground
blankBody.background = style.editorBackground
val bg = SessionUiStyle.Colors.sessionBackground()
background = bg
root.background = bg
root.content.background = bg
sessionContent.background = bg
blankBody.background = bg
load.applyStyle(style)
header.applyStyle(style)
prompt.applyStyle(style)
connection.applyStyle(style)
scroll.applyStyle(style)
empty?.applyStyle(style)
refresh()
}
@@ -5,6 +5,7 @@ import ai.kilocode.client.session.ui.SessionMessageListPanel
import ai.kilocode.client.session.ui.SessionRootPanel
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.components.JBLabel
@@ -223,8 +224,9 @@ internal class SessionScroll(
@RequiresEdt
fun applyStyle(style: SessionEditorStyle) {
this.style = style
component.background = style.editorBackground
component.viewport.background = style.editorBackground
val bg = SessionUiStyle.Colors.sessionBackground()
component.background = bg
component.viewport.background = bg
syncIcon()
messages.applyStyle(style)
val view = component.viewport.view
@@ -64,7 +64,7 @@ class ConnectionPanel(
}
private val label = JBLabel().apply {
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
addMouseListener(click)
}
@@ -83,7 +83,7 @@ class ConnectionPanel(
isOpaque = false
lineWrap = true
wrapStyleWord = true
foreground = UiStyle.Colors.fg()
foreground = SessionUiStyle.Colors.foreground()
}
private val scroll = JBScrollPane(details).apply {
@@ -136,7 +136,7 @@ class ConnectionPanel(
}
private fun showConnecting() {
label.foreground = UiStyle.Colors.weak()
label.foreground = SessionUiStyle.Colors.hint()
label.text = KiloBundle.message("session.connection.connecting")
detail = null
expanded = false
@@ -147,7 +147,7 @@ class ConnectionPanel(
}
private fun showDownloading(percent: Int, version: String?, platform: String?) {
label.foreground = UiStyle.Colors.weak()
label.foreground = SessionUiStyle.Colors.hint()
val pct = percent.coerceIn(0, 100)
label.text = if (version != null && platform != null) {
KiloBundle.message("session.connection.downloading.version", version, platform, pct)
@@ -4,6 +4,7 @@ import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.model.SessionState
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.Centerizer
@@ -33,7 +34,7 @@ class LoadingPanel : JPanel(BorderLayout()), SessionEditorStyleTarget {
else -> {
label.text = KiloBundle.message("session.empty.loading")
label.foreground = UiStyle.Colors.weak()
label.foreground = SessionUiStyle.Colors.hint()
}
}
revalidate()
@@ -156,7 +156,7 @@ class ModifiedFilesView private constructor(
}
val panel = popup.mountFiles(files)
popup.applyStyle(style)
return HeaderPopupBody(panel, owner, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
return HeaderPopupBody(panel, owner, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
}
private class Header {
@@ -189,8 +189,8 @@ class ModifiedFilesView private constructor(
setForeground(glyph, SessionUiStyle.View.Tool.completed())
setFont(title, style.boldEditorFont)
setFont(count, style.transcriptFont)
setForeground(title, UiStyle.Colors.fg())
setForeground(count, UiStyle.Colors.weak())
setForeground(title, SessionUiStyle.Colors.foreground())
setForeground(count, SessionUiStyle.Colors.hint())
}
}
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.SessionModelEvent
import ai.kilocode.client.session.model.SessionState
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.ui.layout.StackAxis
@@ -44,7 +45,7 @@ class ProgressPanel(
foreground = style.editorForeground
}
private val elapsed = JBLabel().apply {
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
}
private val spinner = JBLabel(AnimatedIcon.Default())
private val tick = clock.timer(1000) { syncElapsed() }
@@ -140,7 +141,7 @@ class ProgressPanel(
this.style = style
label.font = style.regularFont
elapsed.font = style.regularFont
elapsed.foreground = UiStyle.Colors.weak()
elapsed.foreground = SessionUiStyle.Colors.hint()
if (state is SessionState.Busy) label.foreground = style.editorForeground
revalidate()
repaint()
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.SessionModel
import ai.kilocode.client.session.model.SessionState
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.SessionViewIcons
import ai.kilocode.client.session.views.base.BaseQuestionView
import ai.kilocode.client.session.views.base.PartHeader
@@ -170,7 +171,7 @@ class RevertBanner(
override fun applyStyle(style: SessionEditorStyle) {
card.applyStyle(style)
title.font = style.headerFont
title.foreground = UiStyle.Colors.fg()
title.foreground = SessionUiStyle.Colors.foreground()
progress?.applyStyle(style)
hint.foreground = UIUtil.getLabelForeground()
notice.foreground = UIUtil.getContextHelpForeground()
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.ui
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import com.intellij.ui.AnimatedIcon
@@ -36,7 +37,7 @@ class RevertProgress(onCancel: () -> Unit) : JPanel(), SessionEditorStyleTarget
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
label.font = style.regularFont
label.foreground = UiStyle.Colors.fg()
label.foreground = SessionUiStyle.Colors.foreground()
cancel.font = style.regularFont
revalidate()
repaint()
@@ -610,7 +610,7 @@ class SessionMessageListPanel(
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
background = style.editorBackground
background = SessionUiStyle.Colors.sessionBackground()
for (view in turnViews.values) view.applyStyle(style)
question?.applyStyle(style)
permission?.applyStyle(style)
@@ -1,5 +1,6 @@
package ai.kilocode.client.session.ui
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.LayeredOverlayPanel
class SessionRootPanel(
@@ -12,5 +13,10 @@ class SessionRootPanel(
class Overlay : LayeredOverlayPanel.Overlay()
class Blocker : LayeredOverlayPanel.Blocker()
class Blocker : LayeredOverlayPanel.Blocker() {
override fun updateUI() {
super.updateUI()
background = SessionUiStyle.Colors.sessionBackground()
}
}
}
@@ -183,7 +183,7 @@ open class AttachmentCard(
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val arc = JBUI.scale(SessionUiStyle.View.Attachment.CORNER_ARC)
g2.color = SessionUiStyle.View.Surface.bgColor()
g2.color = SessionUiStyle.Colors.codeBlockBackground()
g2.fillRoundRect(0, 0, width, height, arc, arc)
g2.color = SessionUiStyle.View.Outline.color()
g2.drawRoundRect(0, 0, width - 1, height - 1, arc, arc)
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.header
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.DiffStatBadge
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
@@ -65,7 +66,7 @@ internal class BranchChangesBadge(
fun applyStyle(style: SessionEditorStyle) {
count.font = style.smallFont
count.foreground = UiStyle.Colors.weak()
count.foreground = SessionUiStyle.Colors.hint()
}
override fun getPreferredSize(): Dimension {
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.ui.header
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.model.ContextUsage
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
@@ -41,9 +42,10 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) {
}
fun applyStyle(style: SessionEditorStyle) {
background = style.editorBackground
val bg = SessionUiStyle.Colors.sessionBackground()
background = bg
foreground = style.editorForeground
meter.background = style.editorBackground
meter.background = bg
used.font = style.smallFont
used.foreground = style.editorForeground
limit.font = style.smallFont
@@ -143,7 +145,7 @@ private class Meter : JComponent() {
fun reservedColor(): Color = shade(0.28f)
private fun shade(alpha: Float): Color {
val base = background ?: UiStyle.Colors.editorBackground()
val base = background ?: SessionUiStyle.Colors.sessionBackground()
val grey = if (UiStyle.Colors.bright(base)) Color.BLACK else Color.WHITE
return UiStyle.Colors.blend(base, grey, alpha)
}
@@ -312,20 +312,21 @@ class SessionHeaderPanel(
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
background = style.editorBackground
val bg = SessionUiStyle.Colors.sessionBackground()
background = bg
foreground = style.editorForeground
top.background = style.editorBackground
top.background = bg
top.isOpaque = true
top.border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.sm(), UiStyle.Gap.md(), UiStyle.Gap.sm())
centerGroup.background = style.editorBackground
centerGroup.background = bg
centerGroup.isOpaque = true
right.background = style.editorBackground
changes.background = style.editorBackground
tokens.background = style.editorBackground
todoRow.background = style.editorBackground
todoBox.background = style.editorBackground
body.background = style.editorBackground
viewport.background = style.editorBackground
right.background = bg
changes.background = bg
tokens.background = bg
todoRow.background = bg
todoBox.background = bg
body.background = bg
viewport.background = bg
title.font = style.boldFont
title.foreground = style.editorForeground
changes.applyStyle(style)
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.mode
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.PickerRow
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.FilledBadgeIcon
import ai.kilocode.client.ui.UiStyle
import com.intellij.icons.AllIcons
@@ -72,7 +73,7 @@ internal class ModePickerRenderer(
): JPanel {
val focus = selected || list.hasFocus() || focused
val fg = UIUtil.getListForeground(selected, focus)
val weak = if (selected) fg else UiStyle.Colors.weak()
val weak = if (selected) fg else SessionUiStyle.Colors.hint()
background = list.background
wrap.update(list, selected, focus)
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.model
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.FilledBadgeIcon
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
@@ -372,13 +373,13 @@ private fun descriptionText(value: String): String = value
private class TagStyle(private val bg: Color) : UiStyle.Badge.Style {
override fun bg() = bg
override fun fg() = UiStyle.Colors.fg()
override fun fg() = SessionUiStyle.Colors.foreground()
}
private fun tagStyle(index: Int): UiStyle.Badge.Style {
if (index % 4 == 0) return UiStyle.Badge.Secondary
val bg = when (index % 4) {
1 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), UiStyle.Colors.fg(), 0.12f)
1 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), SessionUiStyle.Colors.foreground(), 0.12f)
2 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), JBUI.CurrentTheme.Link.Foreground.ENABLED, 0.18f)
else -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), UiStyle.Badge.Primary.bg(), 0.18f)
}
@@ -1,6 +1,7 @@
package ai.kilocode.client.session.ui.style
import ai.kilocode.client.ui.UiStyle
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
@@ -8,8 +9,38 @@ import java.awt.Color
/** Static style tokens owned by the chat session UI. */
object SessionUiStyle {
/**
* The session palette is driven by three authored keys; everything else is derived:
* - [sessionBackground] paints the whole session backdrop (containers stay transparent over it).
* - [codeBlockBackground] is the single raised surface (code blocks, tool/shell output, prompt bubble, prompt input).
* - [foreground] is normal text and links; [hint] is derived from it.
*/
object Colors {
private const val HINT_BOOST = 0.35f
fun sessionBackground(): Color = JBColor.namedColor(
"Kilo.Session.background",
UiStyle.Colors.bg(),
)
fun codeBlockBackground(): Color = JBColor.namedColor(
"Kilo.Session.codeBlockBackground",
JBColor.lazy { UiStyle.Colors.codeBlockBackground(EditorColorsManager.getInstance().globalScheme) },
)
fun foreground(): Color = JBColor.namedColor(
"Kilo.Session.foreground",
UiStyle.Colors.fg(),
)
fun hint(): Color = JBColor.namedColor(
"Kilo.Session.hintForeground",
UiStyle.Colors.blend(UiStyle.Colors.weak(), foreground(), HINT_BOOST),
)
}
object Transcript {
fun bgColor(): Color = UiStyle.Colors.bg()
fun bgColor(): Color = Colors.sessionBackground()
}
/** Geometry for the transcript list and its scroll behavior. */
@@ -69,13 +100,13 @@ object SessionUiStyle {
internal const val HOVER_FILL_ALPHA = 0.10f
object Surface {
fun bgColor(): Color = UiStyle.Colors.editorBackground()
fun bgColor(): Color = Colors.sessionBackground()
fun headerBgColor(): Color = UiStyle.Colors.editorBackground()
fun headerBgColor(): Color = Colors.sessionBackground()
/** Subtle hover fill, softer than the session-view outline. */
fun headerHoverBgColor(): Color = JBColor.lazy {
UiStyle.Colors.blend(headerBgColor(), Outline.hoverColor(), HOVER_FILL_ALPHA)
UiStyle.Colors.blend(Colors.sessionBackground(), Outline.hoverColor(), HOVER_FILL_ALPHA)
}
}
@@ -83,7 +114,7 @@ object SessionUiStyle {
fun color(): Color = UiStyle.Colors.contentBorder()
fun brightColor(): Color = JBColor.lazy {
UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA)
UiStyle.Colors.contrast(Colors.sessionBackground(), BORDER_DELTA)
}
/** Subtle hover outline, stronger than the hover fill. */
@@ -96,15 +127,7 @@ object SessionUiStyle {
/** Prompt input dimensions and chrome inside the session view. */
object Prompt {
/**
* Background of the prompt input and the transcript user-prompt bubble. Uses a dedicated
* theme key so the prompt surface can be restyled independently, defaulting to the
* code-fragment background so the prompt matches rendered code blocks.
*/
fun bgColor(style: SessionEditorStyle): Color = JBColor.namedColor(
"Kilo.Session.Prompt.Background",
UiStyle.Colors.codeBlockBackground(style.editorScheme),
)
fun bgColor(_style: SessionEditorStyle): Color = Colors.codeBlockBackground()
const val EDITOR_LINES = 1
const val EDITOR_CHROME = 16
@@ -145,7 +168,7 @@ object SessionUiStyle {
private const val SCRIM_ALPHA = 210
fun scrim(): Color = JBColor.lazy {
val bg = UiStyle.Colors.bg()
val bg = Colors.sessionBackground()
Color(bg.red, bg.green, bg.blue, SCRIM_ALPHA)
}
@@ -220,11 +243,11 @@ object SessionUiStyle {
*/
const val DIFF_MAX_LINES = 2_000
fun pending(): Color = UiStyle.Colors.weak()
fun pending(): Color = Colors.hint()
fun running(): Color = UiStyle.Colors.fg()
fun running(): Color = Colors.foreground()
fun completed(): Color = UiStyle.Colors.weak()
fun completed(): Color = Colors.hint()
fun error(): Color = UiStyle.Colors.errorLabelForeground()
}
@@ -29,7 +29,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
override val contentId: String = compaction.id
private val text = JBLabel(KiloBundle.message("session.part.compaction")).apply {
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
horizontalAlignment = SwingConstants.CENTER
border = JBUI.Borders.empty(0, UiStyle.Gap.lg())
}
@@ -486,7 +486,7 @@ class MessageView(
g2.fillRoundRect(pt.x, pt.y, box.width, box.height, arc, arc)
// When the prompt shares the session background there is no fill contrast, so draw the
// outline to keep the bubble visible.
if (bg.rgb == style.editorBackground.rgb) {
if (bg.rgb == SessionUiStyle.Colors.sessionBackground().rgb) {
val w = box.width - 1
val h = box.height - 1
g2.color = SessionUiStyle.View.Outline.color()
@@ -245,8 +245,8 @@ class ReasoningView(
md.font = font
changed = md.codeFont != style.editorFamily || changed
md.codeFont = style.editorFamily
changed = md.foreground.rgb != UiStyle.Colors.weak().rgb || changed
md.foreground = UiStyle.Colors.weak()
changed = md.foreground.rgb != SessionUiStyle.Colors.hint().rgb || changed
md.foreground = SessionUiStyle.Colors.hint()
return changed
}
@@ -280,22 +280,22 @@ class ReasoningView(
md.applyStyle(style)
md.font = style.smallEditorFont.deriveFont(Font.ITALIC)
md.codeFont = style.editorFamily
md.foreground = UiStyle.Colors.weak()
md.background = style.editorBackground
md.foreground = SessionUiStyle.Colors.hint()
md.background = SessionUiStyle.Colors.codeBlockBackground()
md.component.border = JBUI.Borders.empty()
md.set(text)
// The shared popup wrapper (HeaderPopupBody) provides the scroll pane, so pass the content
// panel directly instead of nesting a second scroll pane here.
val panel = TrackPanel().apply {
isOpaque = true
background = style.editorBackground
background = SessionUiStyle.Colors.codeBlockBackground()
border = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_HORIZONTAL_PADDING),
)
add(md.component, BorderLayout.CENTER)
}
return HeaderPopupBody(panel, md, style.editorBackground)
return HeaderPopupBody(panel, md, SessionUiStyle.Colors.codeBlockBackground())
}
private fun bodyMaxHeight(): Int {
@@ -360,7 +360,7 @@ class ReasoningParts(
}
val panel = TrackPanel().apply {
isOpaque = true
background = SessionUiStyle.View.Surface.bgColor()
background = SessionUiStyle.Colors.codeBlockBackground()
border = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_HORIZONTAL_PADDING),
@@ -370,8 +370,8 @@ class ReasoningParts(
val scroll = JBScrollPane(panel).apply {
border = JBUI.Borders.empty()
isOpaque = true
background = SessionUiStyle.View.Surface.bgColor()
viewport.background = SessionUiStyle.View.Surface.bgColor()
background = SessionUiStyle.Colors.codeBlockBackground()
viewport.background = SessionUiStyle.Colors.codeBlockBackground()
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
}
@@ -386,8 +386,8 @@ class ReasoningBody(
)
private fun reasoningParts(selection: SessionSelection? = null): ReasoningParts {
val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = UiStyle.Colors.weak() }
val icon = JBLabel(SessionViewIcons.brain).apply { foreground = UiStyle.Colors.weak() }
val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = SessionUiStyle.Colors.hint() }
val icon = JBLabel(SessionViewIcons.brain).apply { foreground = SessionUiStyle.Colors.hint() }
val header = PartHeader().apply {
leading(icon)
left(title)
@@ -7,6 +7,7 @@ import ai.kilocode.client.session.openSessionLink
import ai.kilocode.client.session.model.Content
import ai.kilocode.client.session.model.Text
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.ui.selection.SessionCopyTarget
import ai.kilocode.client.session.ui.selection.SessionSelection
import ai.kilocode.client.session.views.base.PartView
@@ -124,7 +125,7 @@ open class TextView(
protected open fun styleFont(style: SessionEditorStyle) = style.transcriptFont
protected open fun styleBackground(style: SessionEditorStyle) = style.editorBackground
protected open fun styleBackground(style: SessionEditorStyle) = SessionUiStyle.Colors.codeBlockBackground()
protected fun refresh() {
revalidate()
@@ -6,7 +6,10 @@ import com.intellij.ui.components.JBLabel
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component
import java.awt.Container
import java.awt.Cursor
import java.awt.event.ContainerAdapter
import java.awt.event.ContainerEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
@@ -30,6 +33,7 @@ abstract class AbstractSessionPartView(
protected val arrow = JBLabel()
protected val row = JPanel(BorderLayout(SessionUiStyle.View.Header.gap(), 0))
private val bound = linkedSetOf<Component>()
private val watched = linkedSetOf<Component>()
private var body: JComponent? = null
private val click = object : MouseAdapter() {
@@ -38,7 +42,10 @@ abstract class AbstractSessionPartView(
toggle()
}
}
private val mouse = object : MouseAdapter() {
// Hover is tracked across the whole header subtree so leaving the row via any nested
// element (e.g. an unbound file link) still clears the hover fill. Swing only delivers
// mouseExited to the deepest component, so a single listener on the row is not enough.
private val pointer = object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
setHovered(true)
}
@@ -48,6 +55,10 @@ abstract class AbstractSessionPartView(
setHovered(false)
}
}
private val nested = object : ContainerAdapter() {
override fun componentAdded(e: ContainerEvent) = watch(e.child)
override fun componentRemoved(e: ContainerEvent) = unwatch(e.child)
}
init {
layout = BorderLayout()
@@ -56,6 +67,7 @@ abstract class AbstractSessionPartView(
row.add(arrow, BorderLayout.EAST)
add(row, BorderLayout.NORTH)
bindHeader(row, header, arrow)
watch(row)
if (expanded && expandable) add(body(), BorderLayout.CENTER)
if (!expandable) syncExpandable(false) else syncArrow()
}
@@ -148,19 +160,35 @@ abstract class AbstractSessionPartView(
}
private fun bind(component: Component) {
if (bound.contains(component)) return
bound.add(component)
if (!bound.add(component)) return
component.addMouseListener(click)
component.addMouseListener(mouse)
}
private fun unbind(component: Component) {
if (!bound.remove(component)) return
component.removeMouseListener(click)
component.removeMouseListener(mouse)
component.cursor = Cursor.getDefaultCursor()
}
/** Attaches the hover listener to [component] and its whole subtree, keeping up with later children. */
private fun watch(component: Component) {
if (!watched.add(component)) return
component.addMouseListener(pointer)
if (component is Container) {
component.addContainerListener(nested)
component.components.forEach { watch(it) }
}
}
private fun unwatch(component: Component) {
if (!watched.remove(component)) return
component.removeMouseListener(pointer)
if (component is Container) {
component.removeContainerListener(nested)
component.components.forEach { unwatch(it) }
}
}
private fun body(): JComponent {
val item = body
if (item != null) return item
@@ -92,8 +92,8 @@ class BaseQuestionView(
isVisible = false
}
private val headerText: JBTextArea = makeText("", UiStyle.Colors.fg(), bold = true)
private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false).apply {
private val headerText: JBTextArea = makeText("", SessionUiStyle.Colors.foreground(), bold = true)
private val descriptionText: JBTextArea = makeText("", SessionUiStyle.Colors.hint(), bold = false).apply {
isVisible = false
}
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.base
import ai.kilocode.client.session.model.Content
import ai.kilocode.client.session.model.Generic
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import com.intellij.ui.components.JBLabel
@@ -23,7 +24,7 @@ class GenericView private constructor(
override val contentId: String = content.id
init {
label.foreground = UiStyle.Colors.weak()
label.foreground = SessionUiStyle.Colors.hint()
applyStyle(SessionEditorStyle.current())
syncExpandable(false)
border = null
@@ -12,6 +12,7 @@ import ai.kilocode.client.session.views.base.BaseQuestionView
import ai.kilocode.client.session.ui.selection.SessionSelection
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.SessionViewIcons
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.iconButton
@@ -172,7 +173,7 @@ class PermissionView(
this.style = style
card.applyStyle(style)
desc.font = style.hintFont
desc.foreground = UiStyle.Colors.weak()
desc.foreground = SessionUiStyle.Colors.hint()
rules.applyStyle(style)
md?.let { applyCodeStyle(it) }
for (dv in diffViews) {
@@ -276,7 +277,7 @@ class PermissionView(
view.applyStyle(style)
view.font = style.transcriptFont
view.foreground = style.editorForeground
view.background = style.editorBackground
view.background = SessionUiStyle.Colors.codeBlockBackground()
view.preBg = MdCommon.defaults(style).preBg
view.codeFont = style.editorFamily
view.component.border = JBUI.Borders.empty()
@@ -353,7 +354,7 @@ class PermissionView(
caret.isSelectionVisible = false
lineWrap = true
wrapStyleWord = true
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
font = style.hintFont
border = JBUI.Borders.empty()
isVisible = false
@@ -706,7 +707,7 @@ internal class PermissionRulesView(
@RequiresEdt
fun applyStyle(style: SessionEditorStyle) {
hint.font = style.hintFont
hint.foreground = UiStyle.Colors.weak()
hint.foreground = SessionUiStyle.Colors.hint()
field.applyStyle(style)
}
@@ -771,11 +772,11 @@ internal class PermissionRulesView(
ed.setBorder(JBUI.Borders.empty())
ed.scrollPane.border = JBUI.Borders.empty()
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
ed.backgroundColor = style.editorBackground
ed.scrollPane.background = style.editorBackground
ed.backgroundColor = SessionUiStyle.Colors.codeBlockBackground()
ed.scrollPane.background = SessionUiStyle.Colors.codeBlockBackground()
ed.scrollPane.isOpaque = true
ed.scrollPane.viewport.isOpaque = true
ed.scrollPane.viewport.background = style.editorBackground
ed.scrollPane.viewport.background = SessionUiStyle.Colors.codeBlockBackground()
ed.settings.isUseSoftWraps = false
ed.settings.isAdditionalPageAtBottom = false
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
@@ -839,7 +840,7 @@ internal class PermissionRulesView(
val g2 = g.create() as Graphics2D
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val base = UiStyle.Colors.bg()
val base = SessionUiStyle.Colors.sessionBackground()
g2.color = when {
active -> UiStyle.Colors.blend(base, if (approve) UiStyle.Colors.addedForeground() else UiStyle.Colors.removedForeground(), 0.15f)
else -> UiStyle.Colors.actionHoverBackground()
@@ -58,7 +58,7 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
}
private val glyph = JBLabel(SessionViewIcons.bubble)
private val title = JBLabel()
private val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
private val sub = JBLabel().apply { foreground = SessionUiStyle.Colors.hint() }
private val arrow = JBLabel()
private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
isOpaque = false
@@ -197,7 +197,7 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
title.text = KiloBundle.message("session.question.result.title")
val count = result.answers.count { it.isNotEmpty() }
sub.text = KiloBundle.message("session.question.result.answered", count)
sub.foreground = UiStyle.Colors.weak()
sub.foreground = SessionUiStyle.Colors.hint()
}
private fun syncBody() {
@@ -214,7 +214,7 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
}
if (i > 0) row.border = JBUI.Borders.emptyTop(UiStyle.Gap.lg())
val qText = makeText(q, UiStyle.Colors.weak(), false)
val qText = makeText(q, SessionUiStyle.Colors.hint(), false)
qText.alignmentX = Component.LEFT_ALIGNMENT
qText.border = JBUI.Borders.emptyBottom(UiStyle.Gap.xs())
row.add(qText)
@@ -222,7 +222,7 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
val joined = result.answers.getOrNull(i)?.joinToString(", ").orEmpty()
val aText = makeText(
joined.ifBlank { KiloBundle.message("session.question.review.notAnswered") },
UiStyle.Colors.fg(),
SessionUiStyle.Colors.foreground(),
true,
)
aText.alignmentX = Component.LEFT_ALIGNMENT
@@ -185,7 +185,7 @@ class QuestionView(
card.applyStyle(style)
customEditor?.let { ed ->
style.applyTranscriptToField(ed)
ed.background = style.editorBackground
ed.background = SessionUiStyle.Colors.codeBlockBackground()
syncEditorHeight(ed)
}
val changed = texts.fold(false) { acc, item -> setFont(item.first, item.second) || acc }
@@ -223,7 +223,7 @@ class QuestionView(
val total = q.items.size
val shown = minOf(idx + 1, total)
summary.text = KiloBundle.message("session.question.summary", shown, total)
summary.foreground = UiStyle.Colors.weak()
summary.foreground = SessionUiStyle.Colors.hint()
summary.isVisible = total > 1
nav.isVisible = total > 1
topPanel.isVisible = total > 1
@@ -331,7 +331,7 @@ class QuestionView(
layout = BoxLayout(this, BoxLayout.Y_AXIS)
border = JBUI.Borders.emptyBottom(UiStyle.Gap.lg())
}
val qText = text(item.question, UiStyle.Colors.weak())
val qText = text(item.question, SessionUiStyle.Colors.hint())
qText.alignmentX = Component.LEFT_ALIGNMENT
row.add(qText)
@@ -339,7 +339,7 @@ class QuestionView(
val joined = answers.joinToString(", ")
val answer = text(
joined.ifBlank { KiloBundle.message("session.question.review.notAnswered") },
UiStyle.Colors.fg(),
SessionUiStyle.Colors.foreground(),
true,
)
answer.alignmentX = Component.LEFT_ALIGNMENT
@@ -442,7 +442,7 @@ class QuestionView(
addMouseListener(press)
}
val label = text(KiloBundle.message("session.question.custom.label"), UiStyle.Colors.fg(), true)
val label = text(KiloBundle.message("session.question.custom.label"), SessionUiStyle.Colors.foreground(), true)
label.alignmentX = Component.LEFT_ALIGNMENT
label.addMouseListener(press)
col.add(label)
@@ -526,7 +526,7 @@ class QuestionView(
}
selection?.register(ed)?.let(regs::add)
style.applyTranscriptToField(ed)
ed.background = style.editorBackground
ed.background = SessionUiStyle.Colors.codeBlockBackground()
// Pre-fill with saved text. This call also forces lazy document creation so
// that addDocumentListener can install on a non-null document immediately.
@@ -700,13 +700,13 @@ class QuestionView(
layout = if (center) GridBagLayout() else BoxLayout(this, BoxLayout.Y_AXIS)
addMouseListener(press)
}
val label = text(opt.label, UiStyle.Colors.fg(), true)
val label = text(opt.label, SessionUiStyle.Colors.foreground(), true)
label.alignmentX = Component.LEFT_ALIGNMENT
label.addMouseListener(press)
col.add(label)
if (opt.description.isNotBlank()) {
val desc = text(opt.description, UiStyle.Colors.weak())
val desc = text(opt.description, SessionUiStyle.Colors.hint())
desc.alignmentX = Component.LEFT_ALIGNMENT
desc.addMouseListener(press)
col.add(desc)
@@ -52,8 +52,8 @@ class TodoListPanel(
this.style = style
prior.font = style.smallFont
later.font = style.smallFont
prior.foreground = UiStyle.Colors.weak()
later.foreground = UiStyle.Colors.weak()
prior.foreground = SessionUiStyle.Colors.hint()
later.foreground = SessionUiStyle.Colors.hint()
rows.forEachIndexed { index, row -> row.update(items[index], style) }
syncHidden()
}
@@ -136,7 +136,7 @@ class TodoListPanel(
text.font = style.regularFont
text.foreground = when {
!done -> style.editorForeground
else -> UiStyle.Colors.weak()
else -> SessionUiStyle.Colors.hint()
}
}
@@ -103,7 +103,7 @@ class TodoParts(
private fun todoParts(): TodoParts {
val glyph = JBLabel(SessionViewIcons.checklist)
val title = JBLabel(KiloBundle.message("session.part.todo.title"))
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
val sub = JBLabel().apply { foreground = SessionUiStyle.Colors.hint() }
val header = PartHeader().apply {
leading(glyph)
left(title)
@@ -129,7 +129,7 @@ abstract class BaseSearchToolView(
changed = setForeground(parts.glyph, color(item)) || changed
changed = setText(parts.title, toolTitle(item)) || changed
changed = setForeground(parts.title, titleColor(item)) || changed
changed = setForeground(parts.sub, UiStyle.Colors.weak()) || changed
changed = setForeground(parts.sub, SessionUiStyle.Colors.hint()) || changed
changed = syncTargets() || changed
changed = setText(parts.state, stateText(item)) || changed
changed = setForeground(parts.state, color(item)) || changed
@@ -148,7 +148,7 @@ abstract class BaseSearchToolView(
val text = values.getOrNull(index) ?: ""
changed = setVisible(label, text.isNotBlank()) || changed
changed = setTargetText(label, text) || changed
changed = setForeground(label, UiStyle.Colors.fg()) || changed
changed = setForeground(label, SessionUiStyle.Colors.foreground()) || changed
}
return changed
}
@@ -176,7 +176,7 @@ abstract class BaseSearchToolView(
return body.applyStyle(style)
}
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else SessionUiStyle.Colors.foreground()
private fun bodyMaxHeight(): Int {
val body = parts.content ?: return 0
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.tool
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.model.Tool
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import com.intellij.openapi.Disposable
@@ -23,7 +24,7 @@ import javax.swing.JComponent
@RequiresEdt
internal fun diffOverflowPanel(open: () -> Unit): JComponent {
val message = JBLabel(KiloBundle.message("diff.overflow.message")).apply {
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
}
val link = HyperlinkLabel(KiloBundle.message("diff.overflow.open")).apply {
addHyperlinkListener { open() }
@@ -63,7 +63,7 @@ class EditToolView(
)
private val diffAnchor = hoverPlaceholder(diff)
private val filesTag = JBLabel().apply {
foreground = UiStyle.Colors.weak()
foreground = SessionUiStyle.Colors.hint()
font = JBFont.small()
isVisible = false
}
@@ -233,7 +233,7 @@ class EditToolView(
val path = if (count > 1) null else editPath(item)
changed = setFileTarget(parts, path, if (path == null) "" else tail(path)) || changed
changed = setForeground(parts.title, titleColor(item)) || changed
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
changed = setForeground(parts.link, SessionUiStyle.Colors.foreground()) || changed
changed = setText(parts.state, stateText(item)) || changed
changed = setForeground(parts.state, color(item)) || changed
syncDiffAction(count)
@@ -287,7 +287,7 @@ class EditToolView(
// calls rebuild and sets its signature), so a follow-up update() here would be a no-op.
val panel = popup.mount(item)
popup.applyStyle(style)
return HeaderPopupBody(panel, owner, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
return HeaderPopupBody(panel, owner, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
}
override fun dumpLabel() = "EditToolView#$contentId(${labelText()})"
@@ -184,7 +184,7 @@ class PatchBody(
@RequiresEdt
private fun header(file: EditFileChange): JComponent {
val link = FileLinkLabel(openFile).apply {
foreground = UiStyle.Colors.fg()
foreground = SessionUiStyle.Colors.foreground()
font = style.transcriptFont
setTarget(file.path, tail(file.path))
isVisible = true
@@ -207,8 +207,8 @@ class PatchBody(
md.applyStyle(style)
md.font = style.editorFont
md.foreground = style.editorForeground
md.background = style.editorBackground
md.preBg = style.editorBackground
md.background = SessionUiStyle.Colors.codeBlockBackground()
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
md.codeFont = style.editorFamily
md.component.border = JBUI.Borders.empty()
rows.getOrNull(views.indexOf(md))?.let { installGutter(md, it) }
@@ -121,8 +121,8 @@ class ReadToolView(
changed = setText(parts.title, title(item)) || changed
changed = syncSubtitle() || changed
changed = setForeground(parts.title, titleColor(item)) || changed
changed = setForeground(parts.sub, UiStyle.Colors.fg()) || changed
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
changed = setForeground(parts.sub, SessionUiStyle.Colors.foreground()) || changed
changed = setForeground(parts.link, SessionUiStyle.Colors.foreground()) || changed
changed = setText(parts.state, stateText(item)) || changed
changed = setForeground(parts.state, color(item)) || changed
parts.text?.let { changed = setForeground(it, bodyColor()) || changed }
@@ -146,7 +146,7 @@ class ReadToolView(
return true
}
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else SessionUiStyle.Colors.foreground()
private fun bodyMaxHeight(): Int {
val text = parts.text ?: return 0
@@ -174,7 +174,7 @@ class ShellToolView(
changed = setText(parts.title, title(item)) || changed
changed = setText(parts.sub, subtitle(item)) || changed
changed = setForeground(parts.title, titleColor(item)) || changed
changed = setForeground(parts.sub, UiStyle.Colors.weak()) || changed
changed = setForeground(parts.sub, SessionUiStyle.Colors.hint()) || changed
changed = setText(parts.state, stateText(item)) || changed
changed = setForeground(parts.state, color(item)) || changed
return changed
@@ -198,13 +198,13 @@ class ShellToolView(
md.applyStyle(style)
md.font = style.transcriptFont
md.foreground = style.editorForeground
md.background = style.editorBackground
md.preBg = style.editorBackground
md.background = SessionUiStyle.Colors.codeBlockBackground()
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
md.codeFont = style.editorFamily
md.component.border = JBUI.Borders.empty()
md.set(popupMd(formatCommand(cmd)))
padPopup(md.component)
return HeaderPopupBody(md.component, md, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
return HeaderPopupBody(md.component, md, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
}
override fun dumpLabel() = "ShellToolView#$contentId(${labelText()})"
@@ -221,7 +221,7 @@ class TaskToolView(
private var item = tool
val icon = JBLabel()
val title = JBLabel()
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
val sub = JBLabel().apply { foreground = SessionUiStyle.Colors.hint() }
val panel = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply {
isOpaque = false
add(icon, BorderLayout.WEST)
@@ -265,7 +265,7 @@ private class TaskBody(glyph: JBLabel) {
val panel = object : JPanel(BorderLayout()) {
override fun updateUI() {
super.updateUI()
background = SessionUiStyle.View.Surface.bgColor()
background = SessionUiStyle.Colors.codeBlockBackground()
border = taskBodyBorder(glyph)
}
}.apply {
@@ -286,8 +286,8 @@ private class TaskBodyScroll(val body: TaskBody) : JBScrollPane(body.panel) {
override fun updateUI() {
super.updateUI()
border = JBUI.Borders.empty()
background = SessionUiStyle.View.Surface.bgColor()
viewport?.background = SessionUiStyle.View.Surface.bgColor()
background = SessionUiStyle.Colors.codeBlockBackground()
viewport?.background = SessionUiStyle.Colors.codeBlockBackground()
}
}
@@ -313,7 +313,7 @@ private class TaskRows : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()), Scrollable
private fun rowTitleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) {
UiStyle.Colors.errorLabelForeground()
} else {
UiStyle.Colors.weak()
SessionUiStyle.Colors.hint()
}
private fun taskBodyBorder(glyph: JBLabel) = run {
@@ -5,6 +5,7 @@ import ai.kilocode.client.diff.installDiffGutter
import ai.kilocode.client.session.model.Tool
import ai.kilocode.client.session.ui.selection.SessionSelection
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.md.MdCodeBlockFactory
import ai.kilocode.client.ui.md.MdCodeBlockOptions
import ai.kilocode.client.ui.md.MdView
@@ -88,8 +89,8 @@ class ToolMarkdownBody(
md.applyStyle(style)
md.font = font(style)
md.foreground = style.editorForeground
md.background = style.editorBackground
md.preBg = style.editorBackground
md.background = SessionUiStyle.Colors.codeBlockBackground()
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
md.codeFont = style.editorFamily
md.component.border = JBUI.Borders.empty()
chrome(md)
@@ -125,7 +125,7 @@ class FileLinkLabel(
init {
isVisible = false
isFocusable = false
foreground = UiStyle.Colors.fg()
foreground = SessionUiStyle.Colors.foreground()
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
setRequestFocusEnabled(false)
addMouseListener(object : MouseAdapter() {
@@ -191,7 +191,7 @@ class ToolBody private constructor(
var foreground: Color
@RequiresEdt
get() = area?.foreground ?: ed?.foreground ?: UiStyle.Colors.fg()
get() = area?.foreground ?: ed?.foreground ?: SessionUiStyle.Colors.foreground()
@RequiresEdt
set(value) {
area?.foreground = value
@@ -306,8 +306,8 @@ class ToolBody private constructor(
caret.isSelectionVisible = true
lineWrap = wrap
wrapStyleWord = wrap
foreground = if (tool.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
background = SessionUiStyle.View.Surface.bgColor()
foreground = if (tool.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else SessionUiStyle.Colors.foreground()
background = SessionUiStyle.Colors.codeBlockBackground()
border = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
@@ -327,8 +327,8 @@ class ToolBody private constructor(
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
).takeIf { scrolls }
isOpaque = true
background = SessionUiStyle.View.Surface.bgColor()
viewport.background = SessionUiStyle.View.Surface.bgColor()
background = SessionUiStyle.Colors.codeBlockBackground()
viewport.background = SessionUiStyle.Colors.codeBlockBackground()
horizontalScrollBarPolicy = if (scrolls) {
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
} else {
@@ -370,9 +370,9 @@ private class ToolField(value: String, private var style: SessionEditorStyle, pr
ed.setBorder(JBUI.Borders.empty())
ed.scrollPane.border = JBUI.Borders.empty()
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
ed.backgroundColor = SessionUiStyle.View.Surface.bgColor()
ed.scrollPane.background = SessionUiStyle.View.Surface.bgColor()
ed.scrollPane.viewport.background = SessionUiStyle.View.Surface.bgColor()
ed.backgroundColor = SessionUiStyle.Colors.codeBlockBackground()
ed.scrollPane.background = SessionUiStyle.Colors.codeBlockBackground()
ed.scrollPane.viewport.background = SessionUiStyle.Colors.codeBlockBackground()
ed.settings.isUseSoftWraps = false
ed.settings.isAdditionalPageAtBottom = false
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
@@ -404,14 +404,14 @@ internal fun toolParts(
): ToolParts {
val glyph = JBLabel()
val title = clip(JBLabel())
val sub = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() }
val sub = clip(JBLabel()).apply { foreground = SessionUiStyle.Colors.hint() }
val link = clip(FileLinkLabel(openFile))
val slot = Stack.fitHorizontal(SessionUiStyle.View.Header.gap()).apply {
minimumSize = Dimension(0, minimumSize.height)
next(sub)
next(link)
}
val state = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() }
val state = clip(JBLabel()).apply { foreground = SessionUiStyle.Colors.hint() }
val header = PartHeader().apply {
leading(glyph)
left(title)
@@ -426,10 +426,10 @@ internal fun toolParts(
internal fun searchParts(count: Int): ToolParts {
val glyph = JBLabel()
val title = clip(JBLabel())
val sub = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() }
val sub = clip(JBLabel()).apply { foreground = SessionUiStyle.Colors.hint() }
val targets = List(count) {
clip(JBLabel()).apply {
foreground = UiStyle.Colors.fg()
foreground = SessionUiStyle.Colors.foreground()
}
}
val link = clip(FileLinkLabel())
@@ -438,7 +438,7 @@ internal fun searchParts(count: Int): ToolParts {
next(sub)
next(link)
}
val state = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() }
val state = clip(JBLabel()).apply { foreground = SessionUiStyle.Colors.hint() }
val target = Stack.fitHorizontal(SessionUiStyle.View.Header.gap()).apply {
minimumSize = Dimension(0, minimumSize.height)
targets.forEach { next(it) }
@@ -568,7 +568,7 @@ internal fun color(tool: Tool) = when (tool.state) {
internal fun titleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) {
UiStyle.Colors.errorLabelForeground()
} else {
UiStyle.Colors.fg()
SessionUiStyle.Colors.foreground()
}
internal fun stateText(tool: Tool) = when (tool.state) {
@@ -181,7 +181,7 @@ class ToolView(
return body.applyStyle(style)
}
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else SessionUiStyle.Colors.foreground()
private fun bodyMaxHeight(): Int {
val body = parts.content ?: return 0
@@ -56,7 +56,7 @@ class SessionUiLayoutTest : SessionUiTestBase() {
}
fun `test session surfaces use session background from initial render`() {
val bg = SessionEditorStyle.current().editorBackground
val bg = SessionUiStyle.Colors.sessionBackground()
val root = find<SessionRootPanel>(ui)
val pane = scrollComponent() as JBScrollPane
@@ -819,9 +819,16 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
message.paint(graphics)
graphics.dispose()
val line = SessionUiStyle.View.Outline.color().rgb
assertEquals(line, Color(image.getRGB(point.x + box.width / 2, point.y), true).rgb)
assertEquals(line, Color(image.getRGB(point.x + box.width / 2, point.y + box.height - 1), true).rgb)
// The bubble is a filled code-block surface; it only draws an outline when that fill matches
// the session background. Probing the box edges verifies it paints at the wrapped coordinates.
val bg = SessionUiStyle.View.Prompt.bgColor(SessionEditorStyle.current())
val expected = if (bg.rgb == SessionUiStyle.Colors.sessionBackground().rgb) {
SessionUiStyle.View.Outline.color().rgb
} else {
bg.rgb
}
assertEquals(expected, Color(image.getRGB(point.x + box.width / 2, point.y), true).rgb)
assertEquals(expected, Color(image.getRGB(point.x + box.width / 2, point.y + box.height - 1), true).rgb)
}
fun `test created ContentDelta is not double applied`() {
@@ -1,5 +1,6 @@
package ai.kilocode.client.session.ui
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import com.intellij.icons.AllIcons
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@@ -31,11 +32,12 @@ class SessionRootPanelTest : BasePlatformTestCase() {
assertFalse(root.blocker.isVisible)
}
fun `test blocker is opaque and uses panel background`() {
fun `test blocker is opaque and uses session background`() {
val root = SessionRootPanel()
assertTrue(root.blocker.isOpaque)
assertEquals(UiStyle.Colors.bg(), root.blocker.background)
assertEquals(UiStyle.Colors.bg(), SessionUiStyle.Colors.sessionBackground())
assertEquals(SessionUiStyle.Colors.sessionBackground(), root.blocker.background)
}
fun `test root layout fills all immediate children`() {
@@ -76,7 +76,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() {
assertEquals("cache read 75", panel.cacheReadText())
assertEquals("1/2 todos complete", panel.todoText())
assertTrue(panel.todoVisible())
assertEquals(style.editorBackground, panel.background)
assertEquals(SessionUiStyle.Colors.sessionBackground(), panel.background)
assertEquals(
List(panel.foregrounds().size) { style.editorForeground },
panel.foregrounds(),
@@ -288,7 +288,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() {
panel.applyStyle(style)
assertEquals(style.editorBackground, panel.background)
assertEquals(SessionUiStyle.Colors.sessionBackground(), panel.background)
assertEquals(
List(panel.foregrounds().size) { style.editorForeground },
panel.foregrounds(),
@@ -239,7 +239,7 @@ class ShellToolViewTest : BasePlatformTestCase() {
assertEquals(style.transcriptFont.name, view.subtitleFont().name)
assertEquals(style.transcriptFont.size, view.subtitleFont().size)
assertFalse(view.subtitleFont().isBold)
assertEquals(UiStyle.Colors.weak().rgb, view.subtitleForeground().rgb)
assertEquals(SessionUiStyle.Colors.hint().rgb, view.subtitleForeground().rgb)
assertTrue(view.stateFont().size < style.editorSize)
}
@@ -110,7 +110,7 @@ class TaskToolViewTest : BasePlatformTestCase() {
fun `test child tool titles use target color`() {
val view = view(task(children = listOf(child("c1", "read"), child("c2", "grep", ToolExecState.ERROR))))
assertColor(UiStyle.Colors.weak(), titleColor(view, 0))
assertColor(SessionUiStyle.Colors.hint(), titleColor(view, 0))
assertColor(UiStyle.Colors.errorLabelForeground(), titleColor(view, 1))
}
@@ -8,6 +8,7 @@ import ai.kilocode.client.session.model.Tool
import ai.kilocode.client.session.model.ToolExecState
import ai.kilocode.client.session.model.toolKind
import ai.kilocode.client.session.ui.ModifiedFilesView
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.tool.EditToolView
import ai.kilocode.rpc.dto.DiffFileDto
@@ -144,14 +145,22 @@ class TurnViewTest : BasePlatformTestCase() {
assertFalse(mv.isOpaque)
}
fun `test user message uses standard outline color`() {
fun `test user message fills a prompt surface`() {
val mv = MessageView(msg("u1", "user"), openFile)
mv.setSize(120, 48)
val image = BufferedImage(120, 48, BufferedImage.TYPE_INT_ARGB)
mv.paint(image.createGraphics())
assertEquals(SessionUiStyle.View.Outline.color().rgb, image.getRGB(60, 0))
// The user bubble is a filled code-block surface; it only outlines when the fill matches the
// session background (no contrast).
val bg = SessionUiStyle.View.Prompt.bgColor(SessionEditorStyle.current())
val expected = if (bg.rgb == SessionUiStyle.Colors.sessionBackground().rgb) {
SessionUiStyle.View.Outline.color().rgb
} else {
bg.rgb
}
assertEquals(expected, image.getRGB(60, 0))
}
fun `test assistant message remains borderless`() {
@@ -4,11 +4,13 @@ import ai.kilocode.client.session.model.Content
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.SessionViewIcons
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.border.Border
@@ -88,6 +90,8 @@ class AbstractSessionPartViewTest : BasePlatformTestCase() {
}
fun `test header hover fill differs from outline colors`() {
assertEquals(SessionUiStyle.Colors.sessionBackground(), SessionUiStyle.View.Surface.headerBgColor())
assertEquals(SessionUiStyle.Colors.sessionBackground(), SessionUiStyle.View.Surface.bgColor())
assertNotSameColor(SessionUiStyle.View.Surface.headerHoverBgColor(), SessionUiStyle.View.Outline.hoverColor())
assertNotSameColor(SessionUiStyle.View.Surface.headerHoverBgColor(), SessionUiStyle.View.Outline.brightColor())
}
@@ -108,7 +112,24 @@ class AbstractSessionPartViewTest : BasePlatformTestCase() {
assertLine(view.border)
}
private class TestView(content: JLabel, expanded: Boolean = false, expandable: Boolean = true) :
fun `test hover tracks nested header child and clears on leave`() {
val child = JLabel("link")
val header = JPanel(BorderLayout()).apply { add(child, BorderLayout.WEST) }
val view = NestedView(header)
val row = view.component(0) as JPanel
view.setSize(200, 40)
view.doLayout()
// A nested child that is not click-bound (e.g. a file link) must still report hover.
enter(child)
assertEquals(SessionUiStyle.View.Surface.headerHoverBgColor().rgb, row.background.rgb)
// Leaving the row through that nested child clears the fill instead of leaving it stuck.
exit(child, 10_000, 10_000)
assertEquals(SessionUiStyle.View.Surface.headerBgColor().rgb, row.background.rgb)
}
private open class TestView(content: JLabel, expanded: Boolean = false, expandable: Boolean = true) :
PrimarySessionPartView(JLabel("header"), content, expanded, expandable) {
override val contentId = "test"
@@ -117,20 +138,25 @@ class AbstractSessionPartViewTest : BasePlatformTestCase() {
fun arrowIcon(): Icon = arrow.icon
}
private fun TestView.component(index: Int): Component = components[index]
private class NestedView(header: JComponent) : PrimarySessionPartView(header, JLabel("body")) {
override val contentId = "nested"
override fun update(content: Content) {}
}
private fun enter(component: Component) = event(component, MouseEvent.MOUSE_ENTERED)
private fun PrimarySessionPartView.component(index: Int): Component = components[index]
private fun exit(component: Component) = event(component, MouseEvent.MOUSE_EXITED)
private fun enter(component: Component) = event(component, MouseEvent.MOUSE_ENTERED, 1, 1)
private fun event(component: Component, id: Int) {
private fun exit(component: Component, x: Int = 1, y: Int = 1) = event(component, MouseEvent.MOUSE_EXITED, x, y)
private fun event(component: Component, id: Int, x: Int, y: Int) {
component.dispatchEvent(MouseEvent(
component,
id,
System.currentTimeMillis(),
0,
1,
1,
x,
y,
0,
false,
))