fix(jetbrains): separate session backdrop from editor

This commit is contained in:
kirillk
2026-08-12 13:28:20 -04:00
parent 0209c649fd
commit 706a3c3ce7
5 changed files with 79 additions and 21 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep JetBrains session prompts visible in themes where the panel and editor backgrounds match.
+1
View File
@@ -299,6 +299,7 @@ inside the session. This avoids fragile component-hierarchy coupling and prevent
theme-specific artifacts in transparent/rounded Swing painting.
- `SessionRootPanel` is the primary opaque backdrop. It overrides `getBackground()` and returns `SessionUiStyle.Colors.sessionBackground()`.
- `sessionBackground()` follows the panel background, but when that equals the raised editor surface (`codeBlockBackground()`) — e.g. Islands Dark/Darcula, where panel and editor backgrounds are identical — it shifts by `SESSION_DELTA` via `UiStyle.Colors.contrast` (lighter in dark themes, darker in light) so the prompt bubble/input and other raised surfaces stay visible. This is a universal fallback, not a per-theme override.
- `SessionRootPanel.Blocker` is the only other session-background opaque panel. It also overrides `getBackground()` with `sessionBackground()` for modal blocking.
- Scroll panes, viewports, transcript layout panels, message lists, turn containers, wrapper panels, and card bodies should be non-opaque unless they intentionally paint a distinct surface.
- `applyStyle(style)` must not assign session-background colors. It may update fonts, foregrounds, editor colors, and other non-background styling. Background colors that must be dynamic should come from `getBackground()` or custom painting.
@@ -19,20 +19,31 @@ object SessionUiStyle {
private const val HINT_BOOST = 0.35f
/**
* Fallback recess applied to the editor background only if the panel-background key is ever
* missing. [sessionBackground] normally follows the panel background; this keeps it from
* falling through to `JBColor.PanelBackground`'s hardcoded white while defaults are provisional.
* Recess applied to the raised editor surface to derive a distinct backdrop. Used both when
* the panel-background key is missing and when the panel background is identical to the
* raised surface (e.g. Islands Dark/Darcula), so the prompt bubble/input and other raised
* surfaces still stand out. [UiStyle.Colors.contrast] lightens dark themes and darkens light
* themes, matching how generic themes separate the backdrop from editor content.
*/
private const val SESSION_DELTA = 8
/** Whole session backdrop: follows the panel (chrome) background, distinct from raised surfaces. */
fun sessionBackground(): Color = JBColor.namedColor(
"Kilo.Session.background",
JBColor.lazy {
UIManager.getColor("Panel.background")
?: UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), SESSION_DELTA)
},
)
/**
* Whole session backdrop: follows the panel (chrome) background, distinct from raised
* surfaces. When the panel background matches the raised surface — so raised surfaces would
* be invisible against it — the backdrop is shifted by [SESSION_DELTA] instead.
*
* Not a `namedColor`: `JBColor.namedColor` resolves through theme `"*"` wildcard rules by
* suffix (`name.endsWith("background")`), so any theme with a `*.background` rule would
* hijack this key and skip the fallback below. We read an exact override key ourselves and
* otherwise compute the backdrop, wrapped in `JBColor.lazy` so it re-resolves on LaF changes.
*/
fun sessionBackground(): Color = JBColor.lazy {
UIManager.getColor("Kilo.Session.background") ?: run {
val raised = codeBlockBackground()
val panel = UIManager.getColor("Panel.background") ?: raised
if (panel.rgb == raised.rgb) UiStyle.Colors.contrast(raised, SESSION_DELTA) else panel
}
}
/** Single raised surface (code blocks, tool/shell output, prompt bubble, prompt input): the editor background. */
fun codeBlockBackground(): Color = JBColor.namedColor(
@@ -481,17 +481,8 @@ class MessageView(
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
val pt = if (box === this) Point() else SwingUtilities.convertPoint(box, Point(), this)
val bg = SessionUiStyle.View.Prompt.bgColor(style)
g2.color = bg
g2.color = SessionUiStyle.View.Prompt.bgColor(style)
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 == SessionUiStyle.Colors.sessionBackground().rgb) {
val w = box.width - 1
val h = box.height - 1
g2.color = SessionUiStyle.View.Outline.color()
if (w > 0 && h > 0) g2.drawRoundRect(pt.x, pt.y, w, h, arc, arc)
}
} finally {
g2.dispose()
}
@@ -0,0 +1,50 @@
package ai.kilocode.client.session.ui.style
import ai.kilocode.client.ui.UiStyle
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import java.awt.Color
import javax.swing.UIManager
@Suppress("UnstableApiUsage")
class SessionUiStyleTest : BasePlatformTestCase() {
fun `test session background shifts when panel matches the raised editor surface`() {
val raised = SessionUiStyle.Colors.codeBlockBackground()
val previous = UIManager.getColor("Panel.background")
try {
// Force the Islands case: the panel/backdrop is exactly the raised editor surface.
UIManager.put("Panel.background", Color(raised.rgb))
val bg = SessionUiStyle.Colors.sessionBackground()
// When the panel equals the raised surface the backdrop must shift to a distinct color:
// lighter in dark themes, darker in light themes.
assertTrue("Backdrop must not equal the raised surface", raised.rgb != bg.rgb)
val dark = raised.red * 0.299 + raised.green * 0.587 + raised.blue * 0.114 < 128
if (dark) {
assertTrue("Dark themes shift the backdrop lighter", brighter(bg, raised))
} else {
assertTrue("Light themes shift the backdrop darker", darker(bg, raised))
}
} finally {
UIManager.put("Panel.background", previous)
}
}
fun `test session background follows the panel when it differs from the raised surface`() {
val previous = UIManager.getColor("Panel.background")
try {
val panel = UiStyle.Colors.contrast(SessionUiStyle.Colors.codeBlockBackground(), 40)
UIManager.put("Panel.background", panel)
assertEquals(panel.rgb, SessionUiStyle.Colors.sessionBackground().rgb)
} finally {
UIManager.put("Panel.background", previous)
}
}
private fun brighter(shifted: Color, base: Color) =
shifted.red >= base.red && shifted.green >= base.green && shifted.blue >= base.blue
private fun darker(shifted: Color, base: Color) =
shifted.red <= base.red && shifted.green <= base.green && shifted.blue <= base.blue
}