From 5c08bda580eb34b2f7fdb4ba009a0d2e7bb79e74 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 10 Aug 2026 12:07:16 -0400 Subject: [PATCH] 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. --- .../client/session/ui/FileLinkText.kt | 14 ++++++++ .../session/ui/attachment/AttachmentCard.kt | 3 +- .../session/views/PromptAttachmentView.kt | 5 +-- .../client/session/views/tool/ToolSupport.kt | 15 ++++----- .../client/session/ui/SessionUiUpdateTest.kt | 2 +- .../session/views/PromptAttachmentViewTest.kt | 32 +++++++++++++++++-- 6 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/FileLinkText.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/FileLinkText.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/FileLinkText.kt new file mode 100644 index 00000000000..bcc402ef85e --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/FileLinkText.kt @@ -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("${XmlStringUtil.escapeString(text)}") +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/AttachmentCard.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/AttachmentCard.kt index a54e683355a..b5b3d3ebc3b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/AttachmentCard.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/AttachmentCard.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptAttachmentView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptAttachmentView.kt index 919b114fe5d..331499655e3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptAttachmentView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptAttachmentView.kt @@ -27,8 +27,9 @@ class PromptAttachmentView( private val cards = LinkedHashMap() 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 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt index a17b164e31d..3eabce5da94 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt @@ -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("${XmlStringUtil.escapeString(next)}") + 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 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("${XmlStringUtil.escapeString(value)}") } -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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt index 49ff27f5838..e85eb69e4cf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt @@ -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("HvJwtFilter.java:12-40") }) } fun `test source backed image attachment still renders in prompt strip`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PromptAttachmentViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PromptAttachmentViewTest.kt index 457a8cdfd8e..27ad1470af7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PromptAttachmentViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PromptAttachmentViewTest.kt @@ -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().single() + + assertTrue(label.text.contains("HvJwtFilter.java:40-42")) + } + + private fun components(root: Container): List = buildList { + fun visit(comp: java.awt.Component) { + add(comp) + if (comp is Container) comp.components.forEach { visit(it) } + } + visit(root) + } }