feat(jetbrains): align prompt background with code fragments

Give the prompt input and the transcript user-prompt bubble a dedicated
background that defaults to the editor's code-fragment (code block) color,
via a new Kilo.Session.Prompt.Background theme key so it can be restyled
independently. Attachment strip is transparent so the prompt surface shows
through, and the prompt-bar mode/model/effort pickers now blend into that
surface when idle while keeping the standard hover fill. The transcript
bubble drops its outline unless the prompt shares the session background,
in which case it keeps the original outline for contrast.
This commit is contained in:
kirillk
2026-08-10 13:34:46 -04:00
parent 5c08bda580
commit ad933a31df
11 changed files with 87 additions and 41 deletions
@@ -23,6 +23,9 @@ class PromptAttachmentStrip(
private val chips = LinkedHashMap<String, PromptAttachmentChip>()
init {
// Transparent so the prompt shell background shows through instead of the strip
// painting its own panel background above the input surface.
isOpaque = false
border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
isVisible = false
}
@@ -127,11 +127,14 @@ class PromptPanel(
private val INVALID_KEY = CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES
}
val mode = ModePicker()
// Prompt-bar pickers blend into the prompt background when idle and only show the standard
// hover fill on pointer-over (idleFill = null paints nothing behind the label).
val mode = ModePicker().apply { idleFill = null }
val model = ModelPicker().apply {
placement = ModelPicker.Placement.ABOVE
idleFill = null
}
val reasoning = ReasoningPicker()
val reasoning = ReasoningPicker().apply { idleFill = null }
var onReset: () -> Unit = {}
var onChange: () -> Unit = {}
var onAutoApproveToggle: (Boolean) -> Unit = {}
@@ -411,7 +414,7 @@ class PromptPanel(
@RequiresEdt
private fun chrome(ed: EditorEx) {
if (ed.isDisposed) return
style.applyPromptToEditor(ed)
style.applyPromptToEditor(ed, SessionUiStyle.View.Prompt.bgColor(style))
if (ed.isDisposed) return
}
@@ -497,11 +500,12 @@ class PromptPanel(
@RequiresEdt
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
background = style.editorScheme.defaultBackground
shell.background = style.editorScheme.defaultBackground
val bg = SessionUiStyle.View.Prompt.bgColor(style)
background = bg
shell.background = bg
style.applyTranscriptToField(editor)
editor.getEditor(false)?.let(::chrome)
editor.background = style.editorBackground
editor.background = bg
syncEditorHeight()
syncAutoApprove()
syncHighlights()
@@ -80,7 +80,7 @@ data class SessionEditorStyle(
}
/** Apply the visible prompt-input text styling to embedded session editor components. */
fun applyPromptToEditor(editor: EditorEx) {
fun applyPromptToEditor(editor: EditorEx, background: Color = editorBackground) {
if (editor.isDisposed) return
applyTranscriptToEditor(editor)
if (editor.isDisposed) return
@@ -92,11 +92,11 @@ data class SessionEditorStyle(
0,
JBUI.scale(SessionUiStyle.View.Prompt.EDITOR_HORIZONTAL_INSET),
)
editor.backgroundColor = editorBackground
editor.component.background = editorBackground
editor.contentComponent.background = editorBackground
editor.scrollPane.background = editorBackground
editor.scrollPane.viewport.background = editorBackground
editor.backgroundColor = background
editor.component.background = background
editor.contentComponent.background = background
editor.scrollPane.background = background
editor.scrollPane.viewport.background = background
editor.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
editor.scrollPane.revalidate()
editor.scrollPane.repaint()
@@ -96,6 +96,16 @@ 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),
)
const val EDITOR_LINES = 1
const val EDITOR_CHROME = 16
const val SEND_BUTTON_SIZE = 24
@@ -99,7 +99,7 @@ class MessageView(
init {
isOpaque = false
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = style.editorScheme.defaultBackground
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = SessionUiStyle.View.Prompt.bgColor(style)
border = assistantBorder()
// Populate content that already exists (e.g. after loadHistory)
@@ -439,7 +439,7 @@ class MessageView(
@RequiresEdt
override fun applyStyle(style: SessionEditorStyle) {
this.style = style
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = style.editorScheme.defaultBackground
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = SessionUiStyle.View.Prompt.bgColor(style)
for (view in parts.values) view.applyStyle(style)
refresh()
}
@@ -481,14 +481,17 @@ 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 x = pt.x
val y = pt.y
val w = box.width - 1
val h = box.height - 1
g2.color = style.editorScheme.defaultBackground
g2.fillRoundRect(x, y, box.width, box.height, arc, arc)
g2.color = SessionUiStyle.View.Outline.color()
if (w > 0 && h > 0) g2.drawRoundRect(x, y, w, h, arc, arc)
val bg = SessionUiStyle.View.Prompt.bgColor(style)
g2.color = bg
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) {
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()
}
@@ -74,7 +74,7 @@ class PromptView(
override fun styleFont(style: SessionEditorStyle) = style.transcriptFont
override fun styleBackground(style: SessionEditorStyle) = style.editorBackground
override fun styleBackground(style: SessionEditorStyle) = SessionUiStyle.View.Prompt.bgColor(style)
private fun sync() {
md.set(linkifyMentions(buffer.toString(), mentions))
@@ -2,6 +2,7 @@ package ai.kilocode.client.ui
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import java.awt.Color
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.RenderingHints
@@ -11,6 +12,13 @@ import java.awt.event.MouseEvent
open class PickerButton : JBLabel() {
private var over = false
/**
* Idle (unhovered) fill. Defaults to the standard picker surface; set to `null` to paint
* nothing so the picker blends into its container (e.g. the prompt background). The hover
* fill is unaffected.
*/
var idleFill: Color? = UiStyle.Colors.picker()
init {
border = pickerBorder()
background = UiStyle.Colors.picker()
@@ -34,14 +42,17 @@ open class PickerButton : JBLabel() {
}
override fun paintComponent(g: Graphics) {
val g2 = g.create() as Graphics2D
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = if (isEnabled && over) JBUI.CurrentTheme.ActionButton.hoverBackground() else UiStyle.Colors.picker()
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
g2.fillRoundRect(0, 0, width, height, arc, arc)
} finally {
g2.dispose()
val fill = if (isEnabled && over) JBUI.CurrentTheme.ActionButton.hoverBackground() else idleFill
if (fill != null) {
val g2 = g.create() as Graphics2D
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = fill
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
g2.fillRoundRect(0, 0, width, height, arc, arc)
} finally {
g2.dispose()
}
}
super.paintComponent(g)
}
@@ -1,6 +1,8 @@
package ai.kilocode.client.ui
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
@@ -118,6 +120,14 @@ object UiStyle {
/** Uses the editor background so chat cards feel native beside editor content. */
fun editorBackground(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground }
/**
* Background for rendered code fragments (markdown code blocks). Uses the editor's doc
* code-block attribute background and falls back to the editor background when the theme
* leaves it unset.
*/
fun codeBlockBackground(scheme: EditorColorsScheme): Color =
scheme.getAttributes(DefaultLanguageHighlighterColors.DOC_CODE_BLOCK)?.backgroundColor ?: scheme.defaultBackground
/**
* Contained panel background: follows the active theme's text-field/input surface.
* Falls back to the panel background when unavailable.
@@ -94,7 +94,7 @@ internal object MdCommon {
?: fg(style, DefaultLanguageHighlighterColors.DOC_COMMENT)
?: UIUtil.getContextHelpForeground()
val border = color(style, EditorColors.PREVIEW_BORDER_COLOR) ?: UiStyle.Colors.contentBorder()
val blockBg = bg(style, DefaultLanguageHighlighterColors.DOC_CODE_BLOCK) ?: style.editorBackground
val blockBg = UiStyle.Colors.codeBlockBackground(style.editorScheme)
return MdStyle(
font = style.transcriptFont,
foreground = style.editorForeground,
@@ -138,11 +138,11 @@ class PromptPanelTest : BasePlatformTestCase() {
assertEquals(style.transcriptFont.size, font.size)
}
fun `test prompt input uses editor background`() {
fun `test prompt input uses prompt background`() {
val style = SessionEditorStyle.current()
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> })
assertEquals(style.editorScheme.defaultBackground, panel.defaultFocusedComponent.background)
assertEquals(SessionUiStyle.View.Prompt.bgColor(style), panel.defaultFocusedComponent.background)
}
fun `test prompt editor hides floating toolbar`() {
@@ -249,7 +249,12 @@ class PromptPanelTest : BasePlatformTestCase() {
HighlighterColors.TEXT,
TextAttributes(Color(0xEA, 0xEA, 0xEA), bg, null, null, Font.PLAIN),
)
scheme.setAttributes(
DefaultLanguageHighlighterColors.DOC_CODE_BLOCK,
TextAttributes(null, bg, null, null, Font.PLAIN),
)
val style = SessionEditorStyle.create(scheme = scheme)
val promptBg = SessionUiStyle.View.Prompt.bgColor(style)
realize(panel, 260, 400)
val editor = (panel.defaultFocusedComponent as EditorTextField).getEditor(false)!!
@@ -259,11 +264,11 @@ class PromptPanelTest : BasePlatformTestCase() {
panel.applyStyle(style)
assertEquals(bg, panel.defaultFocusedComponent.background)
assertEquals(bg, editor.backgroundColor)
assertEquals(bg, editor.scrollPane.background)
assertEquals(bg, editor.scrollPane.viewport.background)
assertEquals(bg, editor.contentComponent.background)
assertEquals(promptBg, panel.defaultFocusedComponent.background)
assertEquals(promptBg, editor.backgroundColor)
assertEquals(promptBg, editor.scrollPane.background)
assertEquals(promptBg, editor.scrollPane.viewport.background)
assertEquals(promptBg, editor.contentComponent.background)
}
fun `test prompt editor grows when lines are added`() {
@@ -229,14 +229,14 @@ class TextViewTest : BasePlatformTestCase() {
assertEquals(style.editorForeground, view.md.foreground)
}
fun `test prompt view uses transcript font and editor background`() {
fun `test prompt view uses transcript font and prompt background`() {
val style = SessionEditorStyle.create(family = "Courier New", size = 23)
val view = PromptView(Text("p1"))
view.applyStyle(style)
assertEquals(style.transcriptFont, view.md.font)
assertEquals(style.editorBackground, view.md.background)
assertEquals(SessionUiStyle.View.Prompt.bgColor(style), view.md.background)
assertFalse(view.contentOpaque())
}