mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): link read file rows
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Use a subtler platform hover color for JetBrains session cards and match hovered card borders to it.
|
||||
Refine JetBrains session card hover styling and link completed read file rows to files in the IDE.
|
||||
|
||||
+116
-14
@@ -13,12 +13,14 @@ 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
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
@@ -38,7 +40,7 @@ class ToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls)
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
parts.text.text = preview(item)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
@@ -62,7 +64,9 @@ class ToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
fun labelText(): String = listOf(parts.title.text, parts.sub.text, parts.state.text).filter { it.isNotBlank() }.joinToString(" ")
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
fun commandText(): String = command(item)
|
||||
|
||||
@@ -88,6 +92,7 @@ class ToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
var changed = false
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.link, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.text, style.transcriptFont) || changed
|
||||
if (changed) refresh()
|
||||
@@ -138,8 +143,11 @@ class ToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
}
|
||||
|
||||
/** Renders read calls with secondary, borderless chrome. */
|
||||
class ReadToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
SecondarySessionPartView(parts.header, parts.scroll) {
|
||||
class ReadToolView(
|
||||
tool: Tool,
|
||||
openFile: (String) -> Unit = {},
|
||||
private val parts: ToolParts = toolParts(tool, openFile),
|
||||
) : SecondarySessionPartView(parts.header, parts.scroll, expandable = false) {
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = tool.kind == ToolKind.READ
|
||||
@@ -151,7 +159,7 @@ class ReadToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls)
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
parts.text.text = preview(item)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
@@ -172,32 +180,38 @@ class ReadToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
fun labelText(): String = listOf(parts.title.text, parts.sub.text, parts.state.text).filter { it.isNotBlank() }.joinToString(" ")
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
fun bodyText(): String = body(item)
|
||||
internal fun bodyVisible() = parts.scroll.parent === this
|
||||
internal fun hasToggle() = arrow.isVisible
|
||||
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 linkHref() = parts.href
|
||||
internal fun openLink() = parts.openLink()
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.link, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.text, style.transcriptFont) || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
val expand = canExpand(item)
|
||||
var changed = false
|
||||
changed = syncExpandable(expand) || changed
|
||||
changed = setVisible(parts.state, !expand) || changed
|
||||
changed = syncExpandable(false) || changed
|
||||
changed = setVisible(parts.state, true) || changed
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = syncSubtitle() || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
@@ -205,6 +219,29 @@ class ReadToolView(tool: Tool, private val parts: ToolParts = toolParts(tool)) :
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncSubtitle(): Boolean {
|
||||
val target = target(item)?.takeIf { it.type == "file" }
|
||||
if (target != null) {
|
||||
var changed = false
|
||||
if (parts.href != target.path) {
|
||||
parts.href = target.path
|
||||
changed = true
|
||||
}
|
||||
changed = setText(parts.link, tail(target.path).ifBlank { target.path }) || changed
|
||||
changed = show(parts, true) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
var changed = false
|
||||
if (parts.href != null) {
|
||||
parts.href = null
|
||||
changed = true
|
||||
}
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = show(parts, false) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
val value = preview(item)
|
||||
if (parts.text.text == value) return false
|
||||
@@ -228,17 +265,41 @@ class ToolParts(
|
||||
val glyph: JBLabel,
|
||||
val title: JBLabel,
|
||||
val sub: JBLabel,
|
||||
val link: ActionLink,
|
||||
val slot: JPanel,
|
||||
val state: JBLabel,
|
||||
val center: JPanel,
|
||||
val controls: JComponent,
|
||||
val text: JBTextArea,
|
||||
val scroll: JBScrollPane,
|
||||
)
|
||||
private val open: ((String) -> Unit)? = null,
|
||||
) {
|
||||
var href: String? = null
|
||||
|
||||
private fun toolParts(tool: Tool): ToolParts {
|
||||
fun openLink() {
|
||||
val value = href ?: return
|
||||
open?.invoke(value)
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUB_CARD = "sub"
|
||||
private const val LINK_CARD = "link"
|
||||
|
||||
private fun toolParts(tool: Tool, openFile: ((String) -> Unit)? = null): ToolParts {
|
||||
lateinit var parts: ToolParts
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val link = ActionLink("") { parts.openLink() }.apply {
|
||||
isVisible = false
|
||||
isFocusable = false
|
||||
setRequestFocusEnabled(false)
|
||||
}
|
||||
val slot = JPanel(CardLayout()).apply {
|
||||
isOpaque = false
|
||||
add(sub, SUB_CARD)
|
||||
add(link, LINK_CARD)
|
||||
}
|
||||
val state = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false }
|
||||
val text = JBTextArea().apply {
|
||||
@@ -266,12 +327,13 @@ private fun toolParts(tool: Tool): ToolParts {
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
center.add(title, BorderLayout.WEST)
|
||||
center.add(sub, BorderLayout.CENTER)
|
||||
center.add(slot, BorderLayout.CENTER)
|
||||
add(glyph, BorderLayout.WEST)
|
||||
add(center, BorderLayout.CENTER)
|
||||
add(controls, BorderLayout.EAST)
|
||||
}
|
||||
return ToolParts(header, glyph, title, sub, state, center, controls, text, scroll).also {
|
||||
parts = ToolParts(header, glyph, title, sub, link, slot, state, center, controls, text, scroll, openFile)
|
||||
return parts.also {
|
||||
controls.add(it.state)
|
||||
}
|
||||
}
|
||||
@@ -306,6 +368,20 @@ 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
|
||||
return true
|
||||
}
|
||||
|
||||
private fun show(parts: ToolParts, link: Boolean): Boolean {
|
||||
if (parts.link.isVisible == link && parts.sub.isVisible != link) return false
|
||||
(parts.slot.layout as CardLayout).show(parts.slot, if (link) LINK_CARD else SUB_CARD)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.link.text ?: "" else parts.sub.text
|
||||
|
||||
private fun setIcon(label: JBLabel, icon: Icon): Boolean {
|
||||
if (label.icon === icon) return false
|
||||
label.icon = icon
|
||||
@@ -353,10 +429,36 @@ private fun stateText(tool: Tool) = when (tool.state) {
|
||||
}
|
||||
|
||||
private fun readPath(tool: Tool): String {
|
||||
val target = target(tool)
|
||||
if (target != null) {
|
||||
if (target.type == "file") return tail(target.path).ifBlank { target.path }
|
||||
return target.path
|
||||
}
|
||||
val path = tool.input["filePath"] ?: tool.input["path"] ?: tool.title ?: return tool.name
|
||||
return tail(path).ifBlank { path }
|
||||
}
|
||||
|
||||
private data class Target(
|
||||
val path: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
private fun target(tool: Tool): Target? {
|
||||
val out = output(tool)
|
||||
if (out.isBlank()) return null
|
||||
val path = tag(out, "path") ?: return null
|
||||
val type = tag(out, "type") ?: return null
|
||||
return Target(path, type.lowercase())
|
||||
}
|
||||
|
||||
private fun tag(text: String, name: String): String? =
|
||||
Regex("<$name>\\s*([\\s\\S]*?)\\s*</$name>")
|
||||
.find(text)
|
||||
?.groupValues
|
||||
?.getOrNull(1)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun shellTitle(tool: Tool): String =
|
||||
tool.input["description"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["description"]?.takeIf { it.isNotBlank() }
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ object ViewFactory {
|
||||
TodoWriteView.canRender(content) -> TodoWriteView(content)
|
||||
PlanExitView.canRender(content) -> PlanExitView(content, openFile)
|
||||
QuestionResultView.canRender(content) -> QuestionResultView(content)
|
||||
ReadToolView.canRender(content) -> ReadToolView(content)
|
||||
ReadToolView.canRender(content) -> ReadToolView(content, openFile)
|
||||
else -> ToolView(content)
|
||||
}
|
||||
is Compaction -> CompactionView(content)
|
||||
|
||||
+9
-6
@@ -18,6 +18,7 @@ abstract class AbstractSessionPartView(
|
||||
header: JComponent,
|
||||
protected val body: JComponent,
|
||||
expanded: Boolean = false,
|
||||
private val expandable: Boolean = true,
|
||||
) : PartView() {
|
||||
|
||||
protected val arrow = JBLabel()
|
||||
@@ -48,14 +49,14 @@ abstract class AbstractSessionPartView(
|
||||
row.add(arrow, BorderLayout.EAST)
|
||||
add(row, BorderLayout.NORTH)
|
||||
bindHeader(row, header, arrow)
|
||||
if (expanded) add(body, BorderLayout.CENTER)
|
||||
syncArrow()
|
||||
if (expanded && expandable) add(body, BorderLayout.CENTER)
|
||||
if (!expandable) syncExpandable(false) else syncArrow()
|
||||
}
|
||||
|
||||
fun isExpanded(): Boolean = body.parent === this
|
||||
|
||||
fun toggle() {
|
||||
if (!arrow.isVisible) return
|
||||
if (!expandable || !arrow.isVisible) return
|
||||
val changed = if (isExpanded()) collapse() else expand()
|
||||
if (!changed) return
|
||||
syncArrow()
|
||||
@@ -63,6 +64,7 @@ abstract class AbstractSessionPartView(
|
||||
}
|
||||
|
||||
fun expand(): Boolean {
|
||||
if (!expandable) return false
|
||||
if (isExpanded()) return false
|
||||
add(body, BorderLayout.CENTER)
|
||||
return true
|
||||
@@ -75,9 +77,10 @@ abstract class AbstractSessionPartView(
|
||||
}
|
||||
|
||||
fun syncExpandable(expandable: Boolean): Boolean {
|
||||
val changed = setVisible(arrow, expandable)
|
||||
val detached = if (expandable) false else collapse()
|
||||
val cursor = if (expandable) Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) else Cursor.getDefaultCursor()
|
||||
val active = this.expandable && expandable
|
||||
val changed = setVisible(arrow, active)
|
||||
val detached = if (active) false else collapse()
|
||||
val cursor = if (active) Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) else Cursor.getDefaultCursor()
|
||||
val moved = syncCursor(cursor)
|
||||
val icon = syncArrow()
|
||||
return changed || detached || moved || icon
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ abstract class PrimarySessionPartView(
|
||||
header: JComponent,
|
||||
content: JComponent,
|
||||
expanded: Boolean = false,
|
||||
) : AbstractSessionPartView(header, content, expanded) {
|
||||
expandable: Boolean = true,
|
||||
) : AbstractSessionPartView(header, content, expanded, expandable) {
|
||||
init {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@ abstract class SecondarySessionPartView(
|
||||
header: JComponent,
|
||||
content: JComponent,
|
||||
expanded: Boolean = false,
|
||||
) : AbstractSessionPartView(header, content, expanded) {
|
||||
expandable: Boolean = true,
|
||||
) : AbstractSessionPartView(header, content, expanded, expandable) {
|
||||
init {
|
||||
row.isOpaque = true
|
||||
row.background = SessionUiStyle.View.header()
|
||||
|
||||
+46
-4
@@ -29,11 +29,53 @@ class ReadToolViewTest : BasePlatformTestCase() {
|
||||
assertTrue(view.labelText().contains("README.MD"))
|
||||
}
|
||||
|
||||
fun `test read output is secondary collapsible body`() {
|
||||
fun `test read file output renders filename hyperlink`() {
|
||||
val opened = mutableListOf<String>()
|
||||
val path = "/Users/kirillk/work/kilocode/.kilo/worktrees/agreeable-marlin/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt"
|
||||
val t = tool().also {
|
||||
it.output = """
|
||||
<path>$path</path>
|
||||
<type>file</type>
|
||||
<content>
|
||||
content
|
||||
</content>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val view = ReadToolView(t, openFile = { opened.add(it) })
|
||||
|
||||
assertTrue(view.linkVisible())
|
||||
assertEquals("SessionUiLayoutTest.kt", view.linkText())
|
||||
assertEquals(path, view.linkHref())
|
||||
assertTrue(view.labelText().contains("SessionUiLayoutTest.kt"))
|
||||
|
||||
view.openLink()
|
||||
|
||||
assertEquals(listOf(path), opened)
|
||||
}
|
||||
|
||||
fun `test read directory output remains plain text`() {
|
||||
val path = "/Users/kirillk/work/kilocode/packages/kilo-jetbrains"
|
||||
val t = tool().also {
|
||||
it.output = """
|
||||
<path>$path</path>
|
||||
<type>directory</type>
|
||||
<content></content>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val view = ReadToolView(t)
|
||||
|
||||
assertFalse(view.linkVisible())
|
||||
assertNull(view.linkHref())
|
||||
assertTrue(view.labelText().contains(path))
|
||||
}
|
||||
|
||||
fun `test read output is secondary non expandable summary`() {
|
||||
val t = tool().also { it.output = "file contents" }
|
||||
val view = ReadToolView(t)
|
||||
|
||||
assertTrue(view.hasToggle())
|
||||
assertFalse(view.hasToggle())
|
||||
assertFalse(view.isExpanded())
|
||||
assertFalse(view.bodyVisible())
|
||||
assertEquals("file contents", view.bodyText())
|
||||
@@ -41,8 +83,8 @@ class ReadToolViewTest : BasePlatformTestCase() {
|
||||
|
||||
view.toggle()
|
||||
|
||||
assertTrue(view.isExpanded())
|
||||
assertTrue(view.bodyVisible())
|
||||
assertFalse(view.isExpanded())
|
||||
assertFalse(view.bodyVisible())
|
||||
}
|
||||
|
||||
fun `test view factory routes read kind tools to read tool view`() {
|
||||
|
||||
+19
-2
@@ -53,6 +53,22 @@ class AbstractSessionPartViewTest : BasePlatformTestCase() {
|
||||
assertNull(content.parent)
|
||||
}
|
||||
|
||||
fun `test fixed non expandable ignores expansion`() {
|
||||
val content = JLabel("body")
|
||||
val view = TestView(content = content, expanded = true, expandable = false)
|
||||
|
||||
assertFalse(view.isExpanded())
|
||||
assertFalse(view.arrowVisible())
|
||||
assertNull(content.parent)
|
||||
|
||||
view.syncExpandable(true)
|
||||
view.toggle()
|
||||
|
||||
assertFalse(view.isExpanded())
|
||||
assertFalse(view.arrowVisible())
|
||||
assertNull(content.parent)
|
||||
}
|
||||
|
||||
fun `test header hover is subtler than hover outline`() {
|
||||
assertNotSameColor(SessionUiStyle.View.headerHover(), SessionUiStyle.View.hoverLine())
|
||||
assertNotSameColor(SessionUiStyle.View.headerHover(), SessionUiStyle.View.line())
|
||||
@@ -70,11 +86,12 @@ class AbstractSessionPartViewTest : BasePlatformTestCase() {
|
||||
assertEquals(SessionUiStyle.View.line().rgb, paint(view.border).rgb)
|
||||
}
|
||||
|
||||
private class TestView(content: JLabel, expanded: Boolean = false) :
|
||||
PrimarySessionPartView(JLabel("header"), content, expanded) {
|
||||
private class TestView(content: JLabel, expanded: Boolean = false, expandable: Boolean = true) :
|
||||
PrimarySessionPartView(JLabel("header"), content, expanded, expandable) {
|
||||
|
||||
override val contentId = "test"
|
||||
override fun update(content: Content) {}
|
||||
fun arrowVisible() = arrow.isVisible
|
||||
}
|
||||
|
||||
private fun TestView.component(index: Int): Component = components[index]
|
||||
|
||||
Reference in New Issue
Block a user