diff --git a/.changeset/prompt-focus-scroll-polish.md b/.changeset/prompt-focus-scroll-polish.md new file mode 100644 index 00000000000..84fb7749d5f --- /dev/null +++ b/.changeset/prompt-focus-scroll-polish.md @@ -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. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt index c64ca5c0f04..d5b2ae6eb30 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt @@ -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() private var last: String? = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt index 1effc1d3c48..0e5f15eff58 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt @@ -26,6 +26,8 @@ interface SessionManager { val showsBranchBadgeInHeader: Boolean get() = true + val hostedInEditorTab: Boolean get() = false + fun openSession(session: SessionDto) { openSession(SessionRef.Local(session)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index bcb83afd4e9..c71c520a58a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -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 -> diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt index 657260b2933..6055f6aa4f5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt @@ -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, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index a8bff5e6388..e17cb8643fc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -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() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt index f0f5b8f1e24..d49ebd510f5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt @@ -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(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) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index df3444ffef5..bd2e332eb22 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -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)