mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): open session markdown links
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support opening links in JetBrains session markdown transcripts.
|
||||
+6
-1
@@ -36,6 +36,7 @@ import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -223,7 +224,7 @@ class SessionUi(
|
||||
reply = { id, dto -> controller.replyPermission(id, dto) },
|
||||
)
|
||||
login = LoginRequiredView(openProfile = { controller.openProfile() }, dismiss = { controller.dismissLoginRequired() })
|
||||
messageBody = SessionMessageListPanel(controller.model, this, question, permission, login, ::openFile)
|
||||
messageBody = SessionMessageListPanel(controller.model, this, question, permission, login, ::openFile, ::openUrl)
|
||||
header = SessionHeaderPanel(controller, this)
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
@@ -433,6 +434,10 @@ class SessionUi(
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
BrowserUtil.browse(url)
|
||||
}
|
||||
|
||||
private fun onStateChanged(state: SessionState) {
|
||||
prompt.setBusy(state.isBusy())
|
||||
load.setState(state)
|
||||
|
||||
+3
-2
@@ -47,6 +47,7 @@ class SessionMessageListPanel(
|
||||
private val permission: PermissionView? = null,
|
||||
private val login: LoginRequiredView? = null,
|
||||
private val openFile: (String) -> Unit,
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
) : SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
JBUI.insets(
|
||||
@@ -174,7 +175,7 @@ class SessionMessageListPanel(
|
||||
// ------ private event handlers ------
|
||||
|
||||
private fun onTurnAdded(turn: ai.kilocode.client.session.model.Turn) {
|
||||
val tv = TurnView(turn.id, openFile, style)
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl)
|
||||
turnViews[turn.id] = tv
|
||||
for (msgId in turn.messageIds) {
|
||||
val msg = model.message(msgId) ?: continue
|
||||
@@ -225,7 +226,7 @@ class SessionMessageListPanel(
|
||||
removeAll()
|
||||
|
||||
for (turn in model.turns()) {
|
||||
val tv = TurnView(turn.id, openFile, style)
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl)
|
||||
turnViews[turn.id] = tv
|
||||
for (msgId in turn.messageIds) {
|
||||
val msg = model.message(msgId) ?: continue
|
||||
|
||||
+3
-2
@@ -31,6 +31,7 @@ class MessageView(
|
||||
val msg: Message,
|
||||
private val openFile: (String) -> Unit,
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
) : ai.kilocode.client.session.ui.SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
), SessionEditorStyleTarget, SessionView {
|
||||
@@ -165,9 +166,9 @@ class MessageView(
|
||||
}
|
||||
|
||||
private fun view(content: Content) = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) {
|
||||
ViewFactory.createUser(content, openFile)
|
||||
ViewFactory.createUser(content, openFile, openUrl)
|
||||
} else {
|
||||
ViewFactory.create(content, openFile)
|
||||
ViewFactory.create(content, openFile, openUrl)
|
||||
}
|
||||
|
||||
/** Append a streaming delta to the renderer for [contentId]. */
|
||||
|
||||
+6
-1
@@ -23,7 +23,11 @@ import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.Scrollable
|
||||
|
||||
/** Renders reasoning as a secondary collapsible block. */
|
||||
class ReasoningView(reasoning: Reasoning, private val parts: ReasoningParts = reasoningParts()) :
|
||||
class ReasoningView(
|
||||
reasoning: Reasoning,
|
||||
openUrl: (String) -> Unit = {},
|
||||
private val parts: ReasoningParts = reasoningParts(),
|
||||
) :
|
||||
SecondarySessionPartView(parts.header, parts.scroll) {
|
||||
|
||||
override val contentId: String = reasoning.id
|
||||
@@ -37,6 +41,7 @@ class ReasoningView(reasoning: Reasoning, private val parts: ReasoningParts = re
|
||||
bindHeader(parts.title, parts.icon)
|
||||
applyStyle(style)
|
||||
md.opaque = false
|
||||
md.addLinkListener { openUrl(it.href) }
|
||||
md.set(source)
|
||||
parts.panel.add(md.component, BorderLayout.CENTER)
|
||||
sync()
|
||||
|
||||
+6
-1
@@ -12,7 +12,11 @@ import java.awt.BorderLayout
|
||||
*
|
||||
* Supports both full-replacement ([update]) and streaming append ([appendDelta]).
|
||||
*/
|
||||
class TextView(text: Text, transparent: Boolean = false) : PartView() {
|
||||
class TextView(
|
||||
text: Text,
|
||||
transparent: Boolean = false,
|
||||
openUrl: (String) -> Unit = {},
|
||||
) : PartView() {
|
||||
|
||||
override val contentId: String = text.id
|
||||
|
||||
@@ -22,6 +26,7 @@ class TextView(text: Text, transparent: Boolean = false) : PartView() {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
md.opaque = !transparent
|
||||
md.addLinkListener { openUrl(it.href) }
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
if (text.content.isNotEmpty()) md.set(text.content.toString())
|
||||
|
||||
+9
-3
@@ -189,10 +189,14 @@ class ReadToolView(
|
||||
internal fun hasToggle() = arrow.isVisible
|
||||
internal fun horizontalPolicy() = parts.scroll.horizontalScrollBarPolicy
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
internal fun bodyFont() = parts.text.font
|
||||
internal fun linkVisible() = parts.link.isVisible
|
||||
internal fun linkText() = parts.label
|
||||
internal fun linkMarkup() = parts.link.text ?: ""
|
||||
internal fun linkForeground() = parts.link.foreground
|
||||
internal fun linkFont() = parts.link.font
|
||||
internal fun subtitleForeground() = parts.sub.foreground
|
||||
internal fun subtitleFont() = parts.sub.font
|
||||
internal fun linkHref() = parts.href
|
||||
internal fun openLink() = parts.openLink()
|
||||
|
||||
@@ -200,8 +204,8 @@ class ReadToolView(
|
||||
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.sub, style.transcriptFont) || changed
|
||||
changed = setFont(parts.link, style.transcriptFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.text, style.transcriptFont) || changed
|
||||
if (changed) refresh()
|
||||
@@ -216,6 +220,8 @@ class ReadToolView(
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = syncSubtitle() || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.sub, UiStyle.Colors.fg()) || changed
|
||||
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
changed = setForeground(parts.text, bodyColor()) || changed
|
||||
@@ -297,7 +303,7 @@ private fun toolParts(tool: Tool, openFile: ((String) -> Unit)? = null): ToolPar
|
||||
val link = JBLabel().apply {
|
||||
isVisible = false
|
||||
isFocusable = false
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = UiStyle.Colors.fg()
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
setRequestFocusEnabled(false)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ class TurnView(
|
||||
val id: String,
|
||||
private val openFile: (String) -> Unit,
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), SessionEditorStyleTarget {
|
||||
|
||||
constructor(id: String, openFile: (String) -> Unit) : this(id, openFile, SessionEditorStyle.current())
|
||||
@@ -32,7 +33,7 @@ class TurnView(
|
||||
|
||||
/** Add a new [MessageView] for [msg] at the end of this turn. */
|
||||
fun addMessage(msg: Message): MessageView {
|
||||
val view = MessageView(msg, openFile, style)
|
||||
val view = MessageView(msg, openFile, style, openUrl)
|
||||
messages[msg.info.id] = view
|
||||
add(view)
|
||||
revalidate()
|
||||
|
||||
+14
-6
@@ -21,9 +21,13 @@ import ai.kilocode.client.session.views.todo.TodoWriteView
|
||||
* 3. Add a branch here — the exhaustive `when` will surface the gap as a compile error.
|
||||
*/
|
||||
object ViewFactory {
|
||||
fun create(content: Content, openFile: (String) -> Unit): PartView = when (content) {
|
||||
is Text -> TextView(content)
|
||||
is Reasoning -> ReasoningView(content)
|
||||
fun create(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit = {},
|
||||
): PartView = when (content) {
|
||||
is Text -> TextView(content, openUrl = openUrl)
|
||||
is Reasoning -> ReasoningView(content, openUrl = openUrl)
|
||||
is Tool -> when {
|
||||
TodoWriteView.canRender(content) -> TodoWriteView(content)
|
||||
PlanExitView.canRender(content) -> PlanExitView(content, openFile)
|
||||
@@ -36,9 +40,13 @@ 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)
|
||||
fun createUser(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit = {},
|
||||
): PartView = when (content) {
|
||||
is Text -> TextView(content, transparent = true, openUrl = openUrl)
|
||||
else -> create(content, openFile, openUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
@@ -165,6 +165,18 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
assertTrue(mv.part("p1") is TextView)
|
||||
}
|
||||
|
||||
fun `test text markdown link uses panel url opener`() {
|
||||
val urls = mutableListOf<String>()
|
||||
val item = SessionMessageListPanel(model, parent, openFile = openFile, openUrl = { urls.add(it) })
|
||||
model.upsertMessage(msg("a1", "assistant"))
|
||||
model.updateContent("a1", part("p1", "a1", "text", text = "[docs](https://kilocode.ai/docs)"))
|
||||
|
||||
val view = item.findMessage("a1")!!.part("p1") as TextView
|
||||
view.md.simulateLink("https://kilocode.ai/docs")
|
||||
|
||||
assertEquals(listOf("https://kilocode.ai/docs"), urls)
|
||||
}
|
||||
|
||||
fun `test ContentDelta appends text to TextView`() {
|
||||
model.upsertMessage(msg("a1", "assistant"))
|
||||
model.updateContent("a1", part("p1", "a1", "text", text = "hello "))
|
||||
|
||||
+4
-1
@@ -49,7 +49,8 @@ class ReadToolViewTest : BasePlatformTestCase() {
|
||||
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)
|
||||
assertEquals(UiStyle.Colors.fg().rgb, view.linkForeground().rgb)
|
||||
assertEquals(view.linkFont(), view.bodyFont())
|
||||
assertTrue(view.labelText().contains("SessionUiLayoutTest.kt"))
|
||||
|
||||
view.openLink()
|
||||
@@ -71,6 +72,8 @@ class ReadToolViewTest : BasePlatformTestCase() {
|
||||
|
||||
assertFalse(view.linkVisible())
|
||||
assertNull(view.linkHref())
|
||||
assertEquals(UiStyle.Colors.fg().rgb, view.subtitleForeground().rgb)
|
||||
assertEquals(view.subtitleFont(), view.bodyFont())
|
||||
assertTrue(view.labelText().contains(path))
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -163,6 +163,17 @@ class ReasoningViewTest : BasePlatformTestCase() {
|
||||
assertTrue(view.preferredSize.height > 0)
|
||||
}
|
||||
|
||||
fun `test link opens url callback`() {
|
||||
val urls = mutableListOf<String>()
|
||||
val view = ReasoningView(reasoning("p1", done = true, text = "[docs](https://kilocode.ai/docs)"), openUrl = {
|
||||
urls.add(it)
|
||||
})
|
||||
|
||||
view.md.simulateLink("https://kilocode.ai/docs")
|
||||
|
||||
assertEquals(listOf("https://kilocode.ai/docs"), urls)
|
||||
}
|
||||
|
||||
private fun assertEditorSheet(sheet: String, style: SessionEditorStyle) {
|
||||
assertTrue(sheet.contains(style.editorFamily))
|
||||
assertTrue(sheet.contains("${style.editorSize}pt"))
|
||||
|
||||
+9
@@ -113,4 +113,13 @@ class TextViewTest : BasePlatformTestCase() {
|
||||
view.appendDelta("**")
|
||||
assertTrue(view.md.html().contains("<strong>"))
|
||||
}
|
||||
|
||||
fun `test link opens url callback`() {
|
||||
val urls = mutableListOf<String>()
|
||||
val view = TextView(Text("p1"), openUrl = { urls.add(it) })
|
||||
|
||||
view.md.simulateLink("https://kilocode.ai/docs")
|
||||
|
||||
assertEquals(listOf("https://kilocode.ai/docs"), urls)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user