From 706a3c3ce74110f2c0433b7a416ee9ddc02770a4 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 12 Aug 2026 13:28:20 -0400 Subject: [PATCH] fix(jetbrains): separate session backdrop from editor --- .changeset/session-background-contrast.md | 5 ++ packages/kilo-jetbrains/AGENTS.md | 1 + .../client/session/ui/style/SessionUiStyle.kt | 33 ++++++++---- .../client/session/views/MessageView.kt | 11 +--- .../session/ui/style/SessionUiStyleTest.kt | 50 +++++++++++++++++++ 5 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 .changeset/session-background-contrast.md create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyleTest.kt diff --git a/.changeset/session-background-contrast.md b/.changeset/session-background-contrast.md new file mode 100644 index 00000000000..dba72ede1d6 --- /dev/null +++ b/.changeset/session-background-contrast.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Keep JetBrains session prompts visible in themes where the panel and editor backgrounds match. diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index 044b07cf635..7be270ee584 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -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. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index f6ab855d160..92cbac9d7bb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -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( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index 1eb8575ecbf..dbfa7f64c54 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -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() } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyleTest.kt new file mode 100644 index 00000000000..79947eb5d6b --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyleTest.kt @@ -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 +}