fix(jetbrains): polish session prompt chrome

This commit is contained in:
kirillk
2026-08-17 14:13:35 -04:00
parent c22ca3ab7b
commit a417b220fa
8 changed files with 130 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Polish the JetBrains session prompt background, editor-tab focus ring, and centered jump-to-bottom button alignment.
@@ -65,6 +65,7 @@ open class WorktreeSessionEditorManager(
},
) : SessionHost(project, worktree, create, resolve, status, timers, request) {
override val showsBranchBadgeInHeader: Boolean get() = false
override val hostedInEditorTab: Boolean get() = true
private val right = JPanel(BorderLayout())
private val deleting = linkedSetOf<String>()
private var last: String? = null
@@ -26,6 +26,8 @@ interface SessionManager {
val showsBranchBadgeInHeader: Boolean get() = true
val hostedInEditorTab: Boolean get() = false
fun openSession(session: SessionDto) {
openSession(SessionRef.Local(session))
}
@@ -428,6 +428,7 @@ class SessionUi(
onMentions = ::mentionParts,
completion = completion,
cs = cs,
hostedInEditorTab = manager?.hostedInEditorTab == true,
)
connection = ConnectionPanel(this, controller)
root.addOverlay(connection) { pane, child ->
@@ -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
@@ -84,8 +85,10 @@ internal class SessionScroll(
root.addOverlay(jump) { _, child ->
val size = child.preferredSize
val gap = UiStyle.Gap.pad()
val lane = minOf(host.width, SessionUiStyle.SessionLayout.readableWidth(messages, style.transcriptFont))
val right = host.x + (host.width + lane) / 2
Rectangle(
host.x + host.width - size.width - gap,
right - size.width - gap,
host.y + host.height - size.height - gap,
size.width,
size.height,
@@ -117,6 +117,7 @@ class PromptPanel(
private val showSubmit: Boolean = true,
private val approve: Boolean = true,
private val showEnhance: Boolean = true,
private val hostedInEditorTab: Boolean = false,
) : BorderLayoutPanel(), SessionEditorStyleTarget, SendPromptContext, UiDataProvider {
companion object {
@@ -146,7 +147,7 @@ class PromptPanel(
private var style = SessionEditorStyle.current()
private var focused = false
private val shell = BorderLayoutPanel().apply {
isOpaque = true
isOpaque = false
border = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
@@ -274,6 +275,7 @@ class PromptPanel(
init {
applyStyle(style)
syncBorder()
selection?.register(editor)
editor.text = ""
editor.addDocumentListener(object : DocumentListener {
@@ -361,16 +363,31 @@ class PromptPanel(
} else {
JBUI.Borders.customLineTop(SessionUiStyle.View.Prompt.separator())
},
JBUI.Borders.empty(),
JBUI.Borders.empty(0, focusInset(), focusInset(), focusInset()),
)
}
private fun focusInset() = if (hostedInEditorTab) JBUI.scale(SessionUiStyle.View.Prompt.FOCUS_WIDTH) else 0
private fun promptSize(size: Dimension): Dimension {
val chrome = (shell.preferredSize.height - editor.preferredSize.height).coerceAtLeast(0)
val ins = insets
return Dimension(size.width, editor.preferredSize.height + chrome + ins.top + ins.bottom)
}
override fun paintComponent(g: Graphics) {
val g2 = g.create() as Graphics2D
try {
g2.color = SessionUiStyle.Colors.sessionBackground()
g2.fillRect(0, 0, width, height)
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = SessionUiStyle.View.Prompt.bgColor(style)
g2.fill(surface(0f))
} finally {
g2.dispose()
}
}
override fun paintChildren(g: Graphics) {
super.paintChildren(g)
if (!editorFocused()) return
@@ -378,42 +395,44 @@ class PromptPanel(
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val line = JBUI.scale(SessionUiStyle.View.Prompt.FOCUS_WIDTH)
val half = line / 2f
val top = half
val left = half
val right = width - half
val bottom = height - half
val arc = if (rounded && IslandsState.isEnabled()) {
JBUI.scale(JBUI.getInt("Island.arc", SessionUiStyle.View.Prompt.CORNER_ARC)) / 2f
} else {
0f
}
val radius = arc
.coerceAtMost((right - left) / 2f)
.coerceAtMost(bottom - top)
.coerceAtLeast(0f)
val path = Path2D.Float().apply {
moveTo(left, top)
lineTo(right, top)
lineTo(right, bottom - radius)
if (radius > 0f) {
quadTo(right, bottom, right - radius, bottom)
lineTo(left + radius, bottom)
quadTo(left, bottom, left, bottom - radius)
} else {
lineTo(right, bottom)
lineTo(left, bottom)
}
closePath()
}
g2.color = JBUI.CurrentTheme.Focus.focusColor()
g2.stroke = BasicStroke(line.toFloat(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)
g2.draw(path)
g2.draw(surface(line / 2f))
} finally {
g2.dispose()
}
}
private fun surface(inset: Float): Path2D.Float {
val top = inset
val left = inset + insets.left
val right = width - inset - insets.right
val bottom = height - inset - insets.bottom
val arc = if (rounded && IslandsState.isEnabled()) {
JBUI.scale(JBUI.getInt("Island.arc", SessionUiStyle.View.Prompt.CORNER_ARC)) / 2f
} else {
0f
}
val radius = arc
.coerceAtMost((right - left) / 2f)
.coerceAtMost(bottom - top)
.coerceAtLeast(0f)
return Path2D.Float().apply {
moveTo(left, top)
lineTo(right, top)
lineTo(right, bottom - radius)
if (radius > 0f) {
quadTo(right, bottom, right - radius, bottom)
lineTo(left + radius, bottom)
quadTo(left, bottom, left, bottom - radius)
} else {
lineTo(right, bottom)
lineTo(left, bottom)
}
closePath()
}
}
private fun editorFocused(): Boolean {
val ed = editor.getEditor(false) ?: return editor.hasFocus()
return editor.hasFocus() || ed.contentComponent.hasFocus()
@@ -5,9 +5,11 @@ import ai.kilocode.client.session.ui.ModifiedFilesView
import ai.kilocode.client.session.ui.SessionMessageListPanel
import ai.kilocode.client.session.ui.prompt.PromptPanel
import ai.kilocode.client.session.ui.selection.SessionCopyTarget
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.tool.ShellToolView
import ai.kilocode.client.session.views.tool.ToolView
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.DiffFileDto
import ai.kilocode.rpc.dto.MessageErrorDto
@@ -647,6 +649,24 @@ class SessionScrollTest : SessionUiTestBase() {
assertFalse(button.isVisible)
}
fun `test scroll button aligns to centered readable lane`() {
ui.setSize(1600, 600)
showMessages()
fillTranscript(24)
val button = jumpButton()
val bar = scrollBar()
setValue(bar, bottom(bar) / 2)
drainScroll()
val host = scrollComponent().parent as JComponent
val view = find<SessionMessageListPanel>(ui)
val lane = minOf(host.width, SessionUiStyle.SessionLayout.readableWidth(view, SessionEditorStyle.current().transcriptFont))
val right = host.x + (host.width + lane) / 2
assertTrue(button.isVisible)
assertEquals(right, button.x + button.width + UiStyle.Gap.pad())
assertTrue(right < host.x + host.width)
}
fun `test scroll button scrolls transcript to bottom`() {
showMessages()
fillTranscript(24)
@@ -230,6 +230,53 @@ class PromptPanelTest : BasePlatformTestCase() {
}
}
fun `test editor tab prompt reserves focus inset and paints session backdrop`() {
val panel = PromptPanel(
project = project,
onSend = { _, _ -> },
onAbort = {},
onEnhance = { _, _ -> },
hostedInEditorTab = true,
)
realize(panel, 260, 400)
panel.setBounds(0, 0, 260, panel.preferredSize.height)
panel.doLayout()
val ins = panel.insets
assertEquals(JBUI.scale(SessionUiStyle.View.Prompt.FOCUS_WIDTH), ins.bottom)
assertEquals(ins.bottom, ins.left)
assertEquals(ins.bottom, ins.right)
assertEquals(SessionUiStyle.Colors.sessionBackground().rgb, paint(panel, 0, panel.height / 2).rgb)
assertEquals(SessionUiStyle.Colors.sessionBackground().rgb, paint(panel, panel.width - 1, panel.height / 2).rgb)
assertEquals(SessionUiStyle.Colors.sessionBackground().rgb, paint(panel, panel.width / 2, panel.height - 1).rgb)
val editor = (panel.defaultFocusedComponent as EditorTextField).getEditor(false)!!
val current = KeyboardFocusManager.getCurrentKeyboardFocusManager()
val focus = TestFocusManager()
KeyboardFocusManager.setCurrentKeyboardFocusManager(focus)
try {
focus.focus(editor.contentComponent)
editor.contentComponent.focusListeners.forEach {
it.focusGained(FocusEvent(editor.contentComponent, FocusEvent.FOCUS_GAINED))
}
assertEquals(
JBUI.CurrentTheme.Focus.focusColor().rgb,
paint(panel, panel.width / 2, panel.height - 1 - ins.bottom).rgb,
)
assertEquals(
JBUI.CurrentTheme.Focus.focusColor().rgb,
paint(panel, ins.left + 1, panel.height / 2).rgb,
)
assertEquals(
JBUI.CurrentTheme.Focus.focusColor().rgb,
paint(panel, panel.width - ins.right - 1, panel.height / 2).rgb,
)
} finally {
KeyboardFocusManager.setCurrentKeyboardFocusManager(current)
}
}
fun `test applyStyle updates prompt input and height`() {
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> })
val style = SessionEditorStyle.create(family = "Courier New", size = 26)