fix(jetbrains): underline prompt attachment file links

Remove the remaining prompt attachment strip border and reuse the shared underlined file-link styling for attachment chip labels so clickable files look consistent in the transcript.
This commit is contained in:
kirillk
2026-08-10 12:07:16 -04:00
parent aaf2820162
commit 5c08bda580
6 changed files with 56 additions and 15 deletions
@@ -0,0 +1,14 @@
package ai.kilocode.client.session.ui
import com.intellij.xml.util.XmlStringUtil
internal fun fileLinkText(value: String): String = value.lineSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.joinToString(" ")
internal fun fileLinkHtml(value: String): String {
val text = fileLinkText(value)
if (text.isBlank()) return ""
return XmlStringUtil.wrapInHtml("<nobr><u>${XmlStringUtil.escapeString(text)}</u></nobr>")
}
@@ -1,6 +1,7 @@
package ai.kilocode.client.session.ui.attachment
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.fileLinkHtml
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.iconButton
@@ -68,7 +69,7 @@ class AttachmentChip(
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
toolTipText = tip
accessibleContext?.accessibleName = KiloBundle.message("prompt.attachment.open", item.name)
val label = JBLabel(label()).apply {
val label = JBLabel(fileLinkHtml(label())).apply {
icon = attachmentIcon(item.mime, item.name)
iconTextGap = JBUI.scale(SessionUiStyle.View.Attachment.CHIP_ICON_GAP)
toolTipText = tip
@@ -27,8 +27,9 @@ class PromptAttachmentView(
private val cards = LinkedHashMap<String, JComponent>()
private val row = Stack.horizontal(gap = UiStyle.Gap.sm())
private val scroll = JBScrollPane(row).apply {
border = null
viewportBorder = null
// Empty borders remove the visible scroll pane frame; null can be replaced by the current UI.
border = JBUI.Borders.empty()
viewportBorder = JBUI.Borders.empty()
isOpaque = false
viewport.isOpaque = false
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
@@ -7,6 +7,8 @@ import ai.kilocode.client.session.SessionFileOpener
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.ui.fileLinkHtml
import ai.kilocode.client.session.ui.fileLinkText
import ai.kilocode.client.session.ui.selection.SessionSelection
import ai.kilocode.client.session.ui.selection.SessionCopyTarget
import ai.kilocode.client.session.ui.style.SessionEditorStyle
@@ -135,8 +137,8 @@ class FileLinkLabel(
@RequiresEdt
fun setTarget(path: String?, text: String): Boolean {
val next = single(text.ifBlank { path.orEmpty() })
val value = if (next.isBlank()) "" else XmlStringUtil.wrapInHtml("<nobr><u>${XmlStringUtil.escapeString(next)}</u></nobr>")
val next = fileLinkText(text.ifBlank { path.orEmpty() })
val value = fileLinkHtml(next)
var changed = false
if (href != path) {
href = path
@@ -489,7 +491,7 @@ internal fun setText(label: JBLabel, text: String): Boolean {
@RequiresEdt
internal fun setTargetText(label: JBLabel, text: String): Boolean {
val value = single(text)
val value = fileLinkText(text)
if (label.text == value) return false
label.text = value
return true
@@ -511,16 +513,11 @@ private fun <T : JBLabel> clip(label: T): T = label.apply {
}
private fun html(text: String): String {
val value = single(text)
val value = fileLinkText(text)
if (value.isBlank()) return ""
return XmlStringUtil.wrapInHtml("<nobr>${XmlStringUtil.escapeString(value)}</nobr>")
}
private fun single(text: String): String = text.lineSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.joinToString(" ")
@RequiresEdt
internal fun show(parts: ToolParts, link: Boolean): Boolean {
var changed = false
@@ -383,7 +383,7 @@ class SessionUiUpdateTest : BasePlatformTestCase() {
val chip = find(view, AttachmentChip::class.java)
assertNotNull(chip)
assertTrue(findAll(chip!!, JBLabel::class.java).any { it.text == "HvJwtFilter.java:12-40" })
assertTrue(findAll(chip!!, JBLabel::class.java).any { it.text.contains("<u>HvJwtFilter.java:12-40</u>") })
}
fun `test source backed image attachment still renders in prompt strip`() {
@@ -5,6 +5,8 @@ import ai.kilocode.client.session.ui.attachment.AttachmentCardItem
import ai.kilocode.client.session.ui.attachment.AttachmentChip
import ai.kilocode.client.ui.UiStyle
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.components.JBLabel
import java.awt.Container
class PromptAttachmentViewTest : BasePlatformTestCase() {
// The attachment strip should line up with the prompt text horizontally and keep only a
@@ -22,8 +24,14 @@ class PromptAttachmentViewTest : BasePlatformTestCase() {
fun `test attachment scroll pane has no border line`() {
val scroll = PromptAttachmentView("m1") {}.scrollPane()
assertNull(scroll.border)
assertNull(scroll.viewportBorder)
assertEquals(0, scroll.border.getBorderInsets(scroll).top)
assertEquals(0, scroll.border.getBorderInsets(scroll).left)
assertEquals(0, scroll.border.getBorderInsets(scroll).bottom)
assertEquals(0, scroll.border.getBorderInsets(scroll).right)
assertEquals(0, scroll.viewportBorder.getBorderInsets(scroll).top)
assertEquals(0, scroll.viewportBorder.getBorderInsets(scroll).left)
assertEquals(0, scroll.viewportBorder.getBorderInsets(scroll).bottom)
assertEquals(0, scroll.viewportBorder.getBorderInsets(scroll).right)
}
// With the outline removed, the chip owns no internal padding; alignment comes from the
@@ -41,4 +49,24 @@ class PromptAttachmentViewTest : BasePlatformTestCase() {
assertEquals(0, chip.top)
assertEquals(0, chip.bottom)
}
fun `test attachment chip uses file link underline style`() {
val chip = AttachmentChip(
AttachmentCardItem("HvJwtFilter.java", "text/plain", "file:///HvJwtFilter.java"),
file = true,
startLine = 40,
endLine = 42,
)
val label = components(chip).filterIsInstance<JBLabel>().single()
assertTrue(label.text.contains("<u>HvJwtFilter.java:40-42</u>"))
}
private fun components(root: Container): List<java.awt.Component> = buildList {
fun visit(comp: java.awt.Component) {
add(comp)
if (comp is Container) comp.components.forEach { visit(it) }
}
visit(root)
}
}