mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): refine session transcript styling
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refine JetBrains session card hover styling and link completed read file rows to files in the IDE.
|
||||
Refine JetBrains session transcript styling with subtler tool rows, prompt-styled user messages, and underlined read file links that open files in the IDE.
|
||||
|
||||
+40
-12
@@ -11,8 +11,10 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.ui.RoundedLineBorder
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
|
||||
/**
|
||||
* A single message container inside a [TurnView].
|
||||
@@ -45,6 +47,7 @@ class MessageView(
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = style.editorScheme.defaultBackground
|
||||
border = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) {
|
||||
userBorder()
|
||||
} else {
|
||||
@@ -55,7 +58,7 @@ class MessageView(
|
||||
for ((_, content) in msg.parts) {
|
||||
if (content is StepFinish) continue
|
||||
if (isHidden(content)) continue
|
||||
val view = ViewFactory.create(content, openFile)
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
@@ -95,7 +98,7 @@ class MessageView(
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
val view = ViewFactory.create(content, openFile)
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
@@ -107,7 +110,7 @@ class MessageView(
|
||||
val at = components.indexOfFirst { it === existing }.takeIf { it >= 0 } ?: componentCount
|
||||
parts.remove(content.id)
|
||||
remove(existing)
|
||||
val view = ViewFactory.create(content, openFile)
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view, at)
|
||||
@@ -147,7 +150,7 @@ class MessageView(
|
||||
for ((_, content) in msg.parts) {
|
||||
if (content is StepFinish) continue
|
||||
if (isHidden(content)) continue
|
||||
val view = ViewFactory.create(content, openFile)
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
@@ -161,6 +164,12 @@ class MessageView(
|
||||
border = assistantBorder()
|
||||
}
|
||||
|
||||
private fun view(content: Content) = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) {
|
||||
ViewFactory.createUser(content, openFile)
|
||||
} else {
|
||||
ViewFactory.create(content, openFile)
|
||||
}
|
||||
|
||||
/** Append a streaming delta to the renderer for [contentId]. */
|
||||
fun appendDelta(contentId: String, delta: String) {
|
||||
val part = parts[contentId] ?: return
|
||||
@@ -179,22 +188,41 @@ class MessageView(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) background = style.editorScheme.defaultBackground
|
||||
for (view in parts.values) view.applyStyle(style)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (msg.info.role != SessionUiStyle.View.Message.USER_ROLE) {
|
||||
super.paintComponent(g)
|
||||
return
|
||||
}
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
g2.color = style.editorScheme.defaultBackground
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
g2.color = SessionUiStyle.View.line()
|
||||
val w = width - 1
|
||||
val h = height - 1
|
||||
if (w > 0 && h > 0) g2.drawRoundRect(0, 0, w, h, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun userBorder() = JBUI.Borders.compound(
|
||||
RoundedLineBorder(SessionUiStyle.View.line(), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_ARC)),
|
||||
JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_HORIZONTAL_PADDING),
|
||||
),
|
||||
)!!
|
||||
private fun userBorder() = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
|
||||
private fun assistantBorder() = JBUI.Borders.empty()
|
||||
}
|
||||
|
||||
+4
-1
@@ -12,7 +12,7 @@ import java.awt.BorderLayout
|
||||
*
|
||||
* Supports both full-replacement ([update]) and streaming append ([appendDelta]).
|
||||
*/
|
||||
class TextView(text: Text) : PartView() {
|
||||
class TextView(text: Text, transparent: Boolean = false) : PartView() {
|
||||
|
||||
override val contentId: String = text.id
|
||||
|
||||
@@ -21,6 +21,7 @@ class TextView(text: Text) : PartView() {
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
md.opaque = !transparent
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
if (text.content.isNotEmpty()) md.set(text.content.toString())
|
||||
@@ -41,6 +42,8 @@ class TextView(text: Text) : PartView() {
|
||||
/** Current markdown source — used by tests to assert rendered content. */
|
||||
fun markdown(): String = md.markdown()
|
||||
|
||||
internal fun contentOpaque() = md.opaque
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
val changed = md.font != style.transcriptFont || md.codeFont != style.editorFamily
|
||||
if (md.font != style.transcriptFont) md.font = style.transcriptFont
|
||||
|
||||
+24
-11
@@ -9,11 +9,9 @@ import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.ToolKind
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.PrimarySessionPartView
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
@@ -22,8 +20,11 @@ import com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
@@ -32,7 +33,7 @@ import javax.swing.ScrollPaneConstants
|
||||
|
||||
/** Renders non-read tool calls with VS Code-inspired rows/cards. */
|
||||
class ToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
PrimarySessionPartView(parts.header, parts.scroll) {
|
||||
SecondarySessionPartView(parts.header, parts.scroll) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
@@ -189,7 +190,9 @@ class ReadToolView(
|
||||
internal fun horizontalPolicy() = parts.scroll.horizontalScrollBarPolicy
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
internal fun linkVisible() = parts.link.isVisible
|
||||
internal fun linkText() = parts.link.text ?: ""
|
||||
internal fun linkText() = parts.label
|
||||
internal fun linkMarkup() = parts.link.text ?: ""
|
||||
internal fun linkForeground() = parts.link.foreground
|
||||
internal fun linkHref() = parts.href
|
||||
internal fun openLink() = parts.openLink()
|
||||
|
||||
@@ -227,7 +230,7 @@ class ReadToolView(
|
||||
parts.href = target.path
|
||||
changed = true
|
||||
}
|
||||
changed = setText(parts.link, tail(target.path).ifBlank { target.path }) || changed
|
||||
changed = setLinkText(parts, tail(target.path).ifBlank { target.path }) || changed
|
||||
changed = show(parts, true) || changed
|
||||
return changed
|
||||
}
|
||||
@@ -265,7 +268,7 @@ class ToolParts(
|
||||
val glyph: JBLabel,
|
||||
val title: JBLabel,
|
||||
val sub: JBLabel,
|
||||
val link: ActionLink,
|
||||
val link: JBLabel,
|
||||
val slot: JPanel,
|
||||
val state: JBLabel,
|
||||
val center: JPanel,
|
||||
@@ -275,6 +278,7 @@ class ToolParts(
|
||||
private val open: ((String) -> Unit)? = null,
|
||||
) {
|
||||
var href: String? = null
|
||||
var label: String = ""
|
||||
|
||||
fun openLink() {
|
||||
val value = href ?: return
|
||||
@@ -290,10 +294,17 @@ private fun toolParts(tool: Tool, openFile: ((String) -> Unit)? = null): ToolPar
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val link = ActionLink("") { parts.openLink() }.apply {
|
||||
val link = JBLabel().apply {
|
||||
isVisible = false
|
||||
isFocusable = false
|
||||
foreground = UiStyle.Colors.weak()
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
setRequestFocusEnabled(false)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
parts.openLink()
|
||||
}
|
||||
})
|
||||
}
|
||||
val slot = JPanel(CardLayout()).apply {
|
||||
isOpaque = false
|
||||
@@ -368,9 +379,11 @@ private fun setText(label: JBLabel, text: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setText(link: ActionLink, text: String): Boolean {
|
||||
if (link.text == text) return false
|
||||
link.text = text
|
||||
private fun setLinkText(parts: ToolParts, text: String): Boolean {
|
||||
val value = if (text.isBlank()) "" else XmlStringUtil.wrapInHtml("<u>${XmlStringUtil.escapeString(text)}</u>")
|
||||
if (parts.label == text && parts.link.text == value) return false
|
||||
parts.label = text
|
||||
parts.link.text = value
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -380,7 +393,7 @@ private fun show(parts: ToolParts, link: Boolean): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.link.text ?: "" else parts.sub.text
|
||||
private fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.label else parts.sub.text
|
||||
|
||||
private fun setIcon(label: JBLabel, icon: Icon): Boolean {
|
||||
if (label.icon === icon) return false
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ object ViewFactory {
|
||||
is Generic -> GenericView(content)
|
||||
}
|
||||
|
||||
fun createUser(content: Content, openFile: (String) -> Unit): PartView = when (content) {
|
||||
is Text -> TextView(content, transparent = true)
|
||||
else -> create(content, openFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when [view] must be replaced by a new renderer for [content].
|
||||
* This happens when a running question tool (rendered as [ToolView]) completes
|
||||
|
||||
+3
@@ -4,6 +4,7 @@ import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.toolKind
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@@ -47,6 +48,8 @@ class ReadToolViewTest : BasePlatformTestCase() {
|
||||
assertTrue(view.linkVisible())
|
||||
assertEquals("SessionUiLayoutTest.kt", view.linkText())
|
||||
assertEquals(path, view.linkHref())
|
||||
assertTrue(view.linkMarkup().contains("<u>SessionUiLayoutTest.kt</u>"))
|
||||
assertEquals(UiStyle.Colors.weak().rgb, view.linkForeground().rgb)
|
||||
assertTrue(view.labelText().contains("SessionUiLayoutTest.kt"))
|
||||
|
||||
view.openLink()
|
||||
|
||||
+15
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.toolKind
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@@ -79,6 +80,20 @@ class ToolViewTest : BasePlatformTestCase() {
|
||||
assertTrue(view.bodyCreated())
|
||||
}
|
||||
|
||||
fun `test bash tool uses secondary chrome`() {
|
||||
val view = ToolView(tool("p1", "bash", ToolExecState.COMPLETED))
|
||||
val base: Any = view
|
||||
|
||||
assertTrue(base is SecondarySessionPartView)
|
||||
}
|
||||
|
||||
fun `test unknown tool uses secondary chrome`() {
|
||||
val view = ToolView(tool("p1", "mystery", ToolExecState.COMPLETED))
|
||||
val base: Any = view
|
||||
|
||||
assertTrue(base is SecondarySessionPartView)
|
||||
}
|
||||
|
||||
fun `test bash toggle collapses and expands`() {
|
||||
val t = tool("p1", "bash", ToolExecState.COMPLETED).also {
|
||||
it.input = mapOf("command" to "git log")
|
||||
|
||||
+41
@@ -96,6 +96,27 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
assertEquals("assistant", mv.role)
|
||||
}
|
||||
|
||||
fun `test user message uses prompt shell padding`() {
|
||||
val mv = MessageView(msg("u1", "user"), openFile)
|
||||
val ins = mv.border.getBorderInsets(mv)
|
||||
|
||||
assertEquals(JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), ins.top)
|
||||
assertEquals(JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), ins.bottom)
|
||||
assertEquals(JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), ins.left)
|
||||
assertEquals(JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), ins.right)
|
||||
assertFalse(mv.isOpaque)
|
||||
}
|
||||
|
||||
fun `test assistant message remains borderless`() {
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
val ins = mv.border.getBorderInsets(mv)
|
||||
|
||||
assertEquals(0, ins.top)
|
||||
assertEquals(0, ins.bottom)
|
||||
assertEquals(0, ins.left)
|
||||
assertEquals(0, ins.right)
|
||||
}
|
||||
|
||||
fun `test upsertPart adds a new TextView for Text content`() {
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
val text = ai.kilocode.client.session.model.Text("p1")
|
||||
@@ -106,6 +127,26 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
assertTrue(mv.part("p1") is TextView)
|
||||
}
|
||||
|
||||
fun `test user text view is transparent`() {
|
||||
val mv = MessageView(msg("u1", "user"), openFile)
|
||||
val text = ai.kilocode.client.session.model.Text("p1")
|
||||
text.content.append("hello")
|
||||
|
||||
mv.upsertPart(text)
|
||||
|
||||
assertFalse((mv.part("p1") as TextView).contentOpaque())
|
||||
}
|
||||
|
||||
fun `test assistant text view remains opaque`() {
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
val text = ai.kilocode.client.session.model.Text("p1")
|
||||
text.content.append("hello")
|
||||
|
||||
mv.upsertPart(text)
|
||||
|
||||
assertTrue((mv.part("p1") as TextView).contentOpaque())
|
||||
}
|
||||
|
||||
fun `test upsertPart updates existing part rather than adding duplicate`() {
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
val t1 = ai.kilocode.client.session.model.Text("p1").also { it.content.append("v1") }
|
||||
|
||||
Reference in New Issue
Block a user