diff --git a/.changeset/jetbrains-inline-code-urls.md b/.changeset/jetbrains-inline-code-urls.md
new file mode 100644
index 00000000000..e6accb7a366
--- /dev/null
+++ b/.changeset/jetbrains-inline-code-urls.md
@@ -0,0 +1,5 @@
+---
+"@kilocode/kilo-jetbrains": patch
+---
+
+Make `http`/`https` URLs written inside backticks clickable in chat messages. Previously only bare URLs became links, so URLs rendered as inline code — release links, PR links, run URLs — were inert text.
diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdCommon.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdCommon.kt
index 779fb212354..e2eecef6137 100644
--- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdCommon.kt
+++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdCommon.kt
@@ -19,7 +19,12 @@ internal object MdCommon {
private val code = Regex("]*)?>(.*?)", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
private val tag = Regex("<[^>]+>")
private val ref = Regex("(?\"'`]+", RegexOption.IGNORE_CASE)
private val single = setOf("readme.md", "package.json", "tsconfig.json", "jsconfig.json", "kilo.json", "kilo.jsonc")
+ // Entities the markdown renderer emits for characters that cannot appear in a URL; a match that
+ // runs into one of them is cut short so the link stops at the original delimiter.
+ private val entities = listOf("<", ">", """)
+ private const val TRAIL = ".,;:!?"
private const val REF_SEGMENT_LIMIT = 16_384
val tags = listOf(
@@ -36,7 +41,7 @@ internal object MdCommon {
.replace("\r", " ")
fun inlineCode(html: String, opts: MdStyle): String {
- if (!html.contains('<') && !html.contains('.')) return html
+ if (!html.contains('<') && !scan(html)) return html
val color = hex(opts.inlineCodeFg)
val styled = if (html.contains(" REF_SEGMENT_LIMIT || !text.contains('.')) return text
+ if (text.length > REF_SEGMENT_LIMIT || !scan(text)) return text
+ if (!text.contains("://")) return files(text)
+ val out = StringBuilder()
+ var at = 0
+ for (match in url.findAll(text)) {
+ out.append(files(text.substring(at, match.range.first)))
+ val link = cut(match.value)
+ at = match.range.first + link.length
+ // The renderer already escaped the source, so the matched text is a valid attribute value.
+ out.append("$link")
+ }
+ out.append(files(text.substring(at)))
+ return out.toString()
+ }
+
+ private fun files(text: String): String {
+ if (!text.contains('.')) return text
return ref.replace(text) { match ->
val path = match.value
if (!pathish(path)) return@replace path
@@ -173,6 +200,35 @@ internal object MdCommon {
}
}
+ /** Trims a raw URL match down to the part that belongs to the link. */
+ private fun cut(value: String): String {
+ var end = value.length
+ for (item in entities) {
+ val at = value.indexOf(item)
+ if (at in 0 until end) end = at
+ }
+ while (end > 0) {
+ val char = value[end - 1]
+ if (char in TRAIL) {
+ end--
+ continue
+ }
+ val open = when (char) {
+ ')' -> '('
+ ']' -> '['
+ '}' -> '{'
+ else -> break
+ }
+ val head = value.substring(0, end)
+ if (head.count { it == open } >= head.count { it == char }) break
+ end--
+ }
+ return value.substring(0, end)
+ }
+
+ /** True when [text] can contain a file reference or a URL worth scanning for. */
+ private fun scan(text: String): Boolean = text.contains('.') || text.contains("://")
+
private fun pathish(path: String): Boolean {
if (path.contains('/')) return true
val name = path.substringBefore(':')
diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt
index d5cde117805..802abbc3c10 100644
--- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt
+++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt
@@ -344,6 +344,27 @@ class MdViewHybridTest : BasePlatformTestCase() {
assertTrue(html.contains("href=\"native-plan-prompt.txt:37-38\">native-plan-prompt.txt:37-38."))
}
+ fun `test inline code url renders a live anchor that dispatches link events`() {
+ val received = mutableListOf()
+ view.addLinkListener { received.add(it) }
+ view.set("Release PR: `https://example.com/pull/13524`")
+ val pane = htmls().single()
+ val iter = (pane.document as HTMLDocument).getIterator(HTML.Tag.A)
+
+ assertTrue("code span url must render as an anchor", iter.isValid)
+ assertEquals("https://example.com/pull/13524", iter.attributes.getAttribute(HTML.Attribute.HREF))
+
+ val event = HyperlinkEvent(
+ pane,
+ HyperlinkEvent.EventType.ACTIVATED,
+ URI("https://example.com/pull/13524").toURL(),
+ "https://example.com/pull/13524",
+ )
+ pane.hyperlinkListeners.forEach { it.hyperlinkUpdate(event) }
+
+ assertEquals("https://example.com/pull/13524", received.single().href)
+ }
+
fun `test existing links are not nested as file refs`() {
view.set("[prompt](packages/opencode/src/session/prompt.ts)")
val html = view.html()
diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt
index f55b0188ea2..e310bcf99db 100644
--- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt
+++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt
@@ -166,6 +166,76 @@ class MdViewTest : BasePlatformTestCase() {
assertTrue(view.html().contains("https://example.com"))
}
+ fun `test inline code urls become underlined link colored links`() {
+ view.set("Release PR: `https://github.com/Kilo-Org/kilocode/pull/13524`")
+ val code = MdCommon.hex(MdCommon.defaults(SessionEditorStyle.current()).inlineCodeFg)
+ val link = MdCommon.hex(MdCommon.defaults(SessionEditorStyle.current()).linkColor)
+ val html = view.html()
+ val sheet = view.overrideSheet()
+
+ assertTrue(
+ html.contains(
+ "" +
+ "" +
+ "https://github.com/Kilo-Org/kilocode/pull/13524",
+ ),
+ )
+ assertTrue(sheet.contains("a.kilo-url-ref, code a.kilo-url-ref { color: $link; font-family:"))
+ assertTrue(sheet.contains("monospace; text-decoration: underline"))
+ }
+
+ fun `test inline code urls keep query separators in href`() {
+ view.set("Open `https://example.com/a?b=1&c=2` now")
+ val html = view.html()
+
+ assertTrue(html.contains("href=\"https://example.com/a?b=1&c=2\">https://example.com/a?b=1&c=2"))
+ }
+
+ fun `test inline code urls exclude trailing punctuation and unbalanced brackets`() {
+ view.set("See `https://example.com/a.` and `(https://example.com/b)`")
+ val html = view.html()
+
+ assertTrue(html.contains("href=\"https://example.com/a\">https://example.com/a."))
+ assertTrue(html.contains("(https://example.com/b)"))
+ }
+
+ fun `test inline code urls keep balanced brackets inside link`() {
+ view.set("See `https://example.com/a_(b)`")
+ val html = view.html()
+
+ assertTrue(html.contains("href=\"https://example.com/a_(b)\">https://example.com/a_(b)"))
+ }
+
+ fun `test autolinked urls are not wrapped again`() {
+ view.set("Visit https://example.com/a for details")
+ val html = view.html()
+
+ assertFalse(html.contains("kilo-url-ref"))
+ }
+
+ fun `test markdown link urls are not wrapped again`() {
+ view.set("[docs](https://example.com/a)")
+ val html = view.html()
+
+ assertFalse(html.contains("kilo-url-ref"))
+ }
+
+ fun `test fenced code urls are not links`() {
+ view.set("```text\nhttps://example.com/a\n```")
+ val html = view.html()
+
+ assertTrue(html.contains("https://example.com/a"))
+ assertFalse(html.contains("kilo-url-ref"))
+ }
+
+ fun `test inline code urls do not swallow following file refs`() {
+ view.set("`https://example.com/a` then packages/opencode/src/session/prompt.ts")
+ val html = view.html()
+
+ assertTrue(html.contains("href=\"https://example.com/a\">https://example.com/a"))
+ assertTrue(html.contains(""))
+ }
+
// ---- append ----
fun `test append accumulates source`() {