mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-17 17:43:04 +08:00
feat(jetbrains): add hover preview popup to grep, glob, other tools, and todos
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Show the collapsed hover preview on more transcript cards: grep, glob, other tool calls, and to-dos now open the same popup that shell and diffs use.
|
||||
+2
-7
@@ -20,7 +20,6 @@ import ai.kilocode.client.session.views.tool.PatchBody
|
||||
import ai.kilocode.client.session.views.tool.setFont
|
||||
import ai.kilocode.client.session.views.tool.setForeground
|
||||
import ai.kilocode.client.session.views.tool.setIcon
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.DiffBars
|
||||
import ai.kilocode.client.ui.ToolbarButtonAction
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
@@ -112,12 +111,8 @@ class ModifiedFilesView private constructor(
|
||||
override fun copyText(): String? = null
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded() || files.isEmpty()) return null
|
||||
return HeaderPopupRequest(row, build = { buildPopup(files) }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "changes"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("tool", "changes", files.isNotEmpty()) { buildPopup(files) }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
|
||||
+2
-8
@@ -14,7 +14,6 @@ import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
@@ -163,13 +162,8 @@ class ReasoningView(
|
||||
internal fun bodyScrollBottom() = parts.scrollOrNull?.verticalScrollBar?.let { it.maximum - it.visibleAmount } ?: 0
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded()) return null
|
||||
val text = source.takeIf { it.isNotBlank() } ?: return null
|
||||
return HeaderPopupRequest(row, build = { buildPopupBody(text) }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "part" to "reasoning"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("part", "reasoning", source.isNotBlank()) { buildPopupBody(source) }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
|
||||
+65
@@ -1,14 +1,25 @@
|
||||
package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupBody
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockFactory
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockOptions
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.awt.Cursor
|
||||
import java.awt.Font
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
@@ -161,6 +172,60 @@ abstract class AbstractSessionPartView(
|
||||
repaint()
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard collapsed hover-preview request anchored to the card header, or null when the card is
|
||||
* not expandable, is already expanded, or has no [present] preview content. [kind]/[name] are the
|
||||
* telemetry attributes (e.g. `"tool"`/`"bash"`, `"part"`/`"reasoning"`). [body] is built lazily
|
||||
* when the popup actually shows; its disposable is owned by the popup controller and disposed on
|
||||
* hide, so subclasses just build fresh, self-contained content.
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun popup(kind: String, name: String, present: Boolean, body: () -> HeaderPopupBody): HeaderPopupRequest? {
|
||||
if (!expandable || isExpanded() || !present) return null
|
||||
return HeaderPopupRequest(row, body) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", kind to name))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a markdown-backed [HeaderPopupBody] from [markdown]. The created [MdView] is owned by a
|
||||
* fresh disposable that the popup controller disposes when the popup hides, so its editor is always
|
||||
* released. [options] renders through an editor-only code block (shell/diff style); null renders
|
||||
* prose. Height is bounded centrally by the popup panel to the same cap every popup shares; width
|
||||
* uses the wide or normal popup cap.
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun markdownPopupBody(
|
||||
style: SessionEditorStyle,
|
||||
markdown: String,
|
||||
wide: Boolean = true,
|
||||
options: MdCodeBlockOptions? = null,
|
||||
font: Font = style.editorFont,
|
||||
foreground: Color = style.editorForeground,
|
||||
link: ((String) -> Unit)? = null,
|
||||
afterSet: (MdView) -> Unit = {},
|
||||
): HeaderPopupBody {
|
||||
val owner = Disposer.newDisposable("Header popup body")
|
||||
val md = if (options != null) {
|
||||
MdViewFactory.create(style, null, MdCodeBlockFactory.default(options))
|
||||
} else {
|
||||
MdViewFactory.create(style, null)
|
||||
}
|
||||
Disposer.register(owner, md)
|
||||
link?.let { l -> md.addLinkListener { l(it.href) } }
|
||||
md.applyStyle(style)
|
||||
md.font = font
|
||||
md.foreground = foreground
|
||||
md.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.codeFont = style.editorFamily
|
||||
md.component.border = JBUI.Borders.empty()
|
||||
md.set(markdown)
|
||||
afterSet(md)
|
||||
val width = if (wide) SessionUiStyle.View.Popup.WIDE_MAX_WIDTH else SessionUiStyle.View.Popup.MAX_WIDTH
|
||||
return HeaderPopupBody(md.component, owner, SessionUiStyle.Colors.codeBlockBackground(), width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Header background, hovered or not. The header keeps the same rounded fill whether the card is
|
||||
* collapsed or expanded — only the arrow toggles — because the card no longer draws an outline
|
||||
|
||||
+12
@@ -4,6 +4,7 @@ import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
@@ -11,7 +12,9 @@ import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Font
|
||||
import javax.swing.JComponent
|
||||
@@ -38,6 +41,15 @@ class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
val md = todoMarkdown(item.todos)
|
||||
return popup("part", "todo", md.isNotBlank()) { markdownPopupBody(style, md) }
|
||||
}
|
||||
|
||||
private fun todoMarkdown(todos: List<TodoDto>): String =
|
||||
todos.joinToString("\n") { "- [${if (it.status == "completed") "x" else " "}] ${it.content}" }
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
|
||||
+9
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.tool
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
@@ -64,6 +65,14 @@ abstract class BaseSearchToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
val md = toolBodyMarkdown(item)
|
||||
return popup("tool", item.name, md.isNotBlank()) {
|
||||
markdownPopupBody(style, md, options = POPUP_OPTS, foreground = bodyColor())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text).plus(targetTexts()).plus(parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
+2
-8
@@ -17,7 +17,6 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.DiffStatBadge
|
||||
import ai.kilocode.client.ui.ToolbarButtonAction
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
@@ -196,13 +195,8 @@ class EditToolView(
|
||||
internal fun codeEditors(): List<EditorTextField> = body.codeEditors()
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded()) return null
|
||||
if (editDiff(item).isBlank()) return null
|
||||
return HeaderPopupRequest(row, build = { buildPopupBody() }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "edit"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("tool", "edit", editDiff(item).isNotBlank()) { buildPopupBody() }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
|
||||
+16
-30
@@ -2,7 +2,6 @@ package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.session.ui.SessionContentPanel
|
||||
import ai.kilocode.client.session.ui.SessionSurfacePanel
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupBody
|
||||
@@ -12,9 +11,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockBorder
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockFactory
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockOptions
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import ai.kilocode.client.ui.md.hybrid.MdTerminal
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
@@ -147,11 +144,8 @@ class ShellToolView(
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded()) return null
|
||||
val cmd = command(item).takeIf { it.isNotBlank() } ?: return null
|
||||
return HeaderPopupRequest(row, build = { buildPopupBody(cmd) }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "bash"))
|
||||
}
|
||||
val cmd = command(item)
|
||||
return popup("tool", "bash", cmd.isNotBlank()) { buildPopupBody(cmd) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
@@ -185,29 +179,14 @@ class ShellToolView(
|
||||
private fun syncBody(): Boolean = body.update(item)
|
||||
|
||||
@RequiresEdt
|
||||
private fun buildPopupBody(cmd: String): HeaderPopupBody {
|
||||
val md = MdViewFactory.create(
|
||||
private fun buildPopupBody(cmd: String): HeaderPopupBody =
|
||||
markdownPopupBody(
|
||||
style,
|
||||
null,
|
||||
MdCodeBlockFactory.default(
|
||||
MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.None,
|
||||
verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
editorOnly = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
md.applyStyle(style)
|
||||
md.font = style.transcriptFont
|
||||
md.foreground = style.editorForeground
|
||||
md.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.codeFont = style.editorFamily
|
||||
md.component.border = JBUI.Borders.empty()
|
||||
md.set(popupShellMarkdown(item, cmd))
|
||||
padPopup(md.component)
|
||||
return HeaderPopupBody(md.component, md, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
|
||||
}
|
||||
popupShellMarkdown(item, cmd),
|
||||
options = SHELL_POPUP_OPTS,
|
||||
font = style.transcriptFont,
|
||||
foreground = style.editorForeground,
|
||||
) { padPopup(it.component) }
|
||||
|
||||
override fun dumpLabel() = "ShellToolView#$contentId(${labelText()})"
|
||||
|
||||
@@ -299,6 +278,13 @@ class ShellBody(selection: SessionSelection?) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Editor-only code block used by the collapsed shell hover popup (uncapped; the popup scrolls). */
|
||||
private val SHELL_POPUP_OPTS = MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.None,
|
||||
verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
editorOnly = true,
|
||||
)
|
||||
|
||||
private fun shellSection(selection: SessionSelection?, render: (Tool) -> String) = ToolMarkdownBody(
|
||||
MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.None,
|
||||
|
||||
+13
@@ -647,6 +647,19 @@ internal fun preview(tool: Tool): String = if (tool.name == "bash") shellPreview
|
||||
|
||||
internal fun body(tool: Tool): String = if (tool.name == "bash") shellBody(tool) else plainBody(tool)
|
||||
|
||||
/** Fenced markdown of a tool's body text for the collapsed hover popup; empty when nothing to show. */
|
||||
internal fun toolBodyMarkdown(tool: Tool): String {
|
||||
val text = body(tool)
|
||||
if (text.isBlank()) return ""
|
||||
val fence = fence(text)
|
||||
return buildString {
|
||||
append(fence).append('\n')
|
||||
append(text)
|
||||
if (!text.endsWith('\n')) append('\n')
|
||||
append(fence)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shellPreview(tool: Tool): String {
|
||||
val cmd = command(tool)
|
||||
val out = output(tool)
|
||||
|
||||
+9
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.tool
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
@@ -70,6 +71,14 @@ class ToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
val md = toolBodyMarkdown(item)
|
||||
return popup("tool", item.name, md.isNotBlank()) {
|
||||
markdownPopupBody(style, md, options = POPUP_OPTS, foreground = bodyColor())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
+35
@@ -8,8 +8,12 @@ import ai.kilocode.client.session.views.tool.GlobToolView
|
||||
import ai.kilocode.client.session.views.tool.ReadToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
@@ -155,6 +159,37 @@ class GlobToolViewTest : BasePlatformTestCase() {
|
||||
assertFalse(ViewFactory.shouldReplace(GlobToolView(glob), glob))
|
||||
}
|
||||
|
||||
fun `test glob header popup previews matches when collapsed`() {
|
||||
val view = track(GlobToolView(tool().also {
|
||||
it.input = mapOf("pattern" to "**/*.kt")
|
||||
it.output = "src/A.kt\nsrc/B.kt"
|
||||
}))
|
||||
val body = view.headerPopup()!!.build()
|
||||
try {
|
||||
val editors = popupEditors(body.component)
|
||||
editors.forEach { it.getEditor(true) }
|
||||
assertEquals(listOf("src/A.kt\nsrc/B.kt"), editors.map { it.text })
|
||||
assertTrue(body.component.preferredSize.height in 1..JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT))
|
||||
} finally {
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test glob header popup is absent when expanded and leaks no editors`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
val view = track(GlobToolView(tool().also { it.output = "src/A.kt" }))
|
||||
assertNotNull(view.headerPopup())
|
||||
repeat(20) {
|
||||
val body = view.headerPopup()!!.build()
|
||||
popupEditors(body.component).forEach { it.getEditor(true) }
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
view.toggle()
|
||||
assertNull(view.headerPopup())
|
||||
}
|
||||
|
||||
private fun tool() = Tool("p1", "glob", toolKind("glob")).also { it.state = ToolExecState.COMPLETED }
|
||||
|
||||
private fun track(view: GlobToolView): GlobToolView {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ai.kilocode.client.session.views
|
||||
|
||||
import com.intellij.ui.EditorTextField
|
||||
import javax.swing.JComponent
|
||||
|
||||
/** Collects every code editor nested anywhere inside a built header popup body. */
|
||||
internal fun popupEditors(root: JComponent): List<EditorTextField> {
|
||||
val found = mutableListOf<EditorTextField>()
|
||||
fun visit(component: JComponent) {
|
||||
if (component is EditorTextField) found.add(component)
|
||||
component.components.filterIsInstance<JComponent>().forEach(::visit)
|
||||
}
|
||||
visit(root)
|
||||
return found
|
||||
}
|
||||
+37
@@ -10,9 +10,11 @@ import ai.kilocode.client.session.views.tool.ReadToolView
|
||||
import ai.kilocode.client.session.views.tool.SearchToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Container
|
||||
import java.awt.Dimension
|
||||
@@ -205,6 +207,41 @@ class SearchToolViewTest : BasePlatformTestCase() {
|
||||
assertFalse(ViewFactory.shouldReplace(SearchToolView(search), search))
|
||||
}
|
||||
|
||||
fun `test search header popup previews results when collapsed`() {
|
||||
val view = track(SearchToolView(tool().also {
|
||||
it.input = mapOf("pattern" to "foo")
|
||||
it.output = "a.kt:1: foo\nb.kt:2: foo"
|
||||
}))
|
||||
val body = view.headerPopup()!!.build()
|
||||
try {
|
||||
val editors = popupEditors(body.component)
|
||||
editors.forEach { it.getEditor(true) }
|
||||
assertEquals(listOf("a.kt:1: foo\nb.kt:2: foo"), editors.map { it.text })
|
||||
assertTrue(body.component.preferredSize.height in 1..JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT))
|
||||
} finally {
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test search header popup is absent when expanded`() {
|
||||
val view = track(SearchToolView(tool().also { it.output = "hit" }))
|
||||
assertNotNull(view.headerPopup())
|
||||
view.toggle()
|
||||
assertNull(view.headerPopup())
|
||||
}
|
||||
|
||||
fun `test search header popup leaks no editors after churn`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
val view = track(SearchToolView(tool().also { it.output = "hit" }))
|
||||
repeat(20) {
|
||||
val body = view.headerPopup()!!.build()
|
||||
popupEditors(body.component).forEach { it.getEditor(true) }
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
private fun layout(root: Container) {
|
||||
root.doLayout()
|
||||
root.components.filterIsInstance<Container>().forEach { layout(it) }
|
||||
|
||||
+41
@@ -10,9 +10,11 @@ import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.scale.JBUIScale
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
@@ -407,6 +409,45 @@ class ToolViewTest : BasePlatformTestCase() {
|
||||
assertEquals("part99", view.contentId)
|
||||
}
|
||||
|
||||
// ---- header popup ------
|
||||
|
||||
fun `test tool header popup previews output when collapsed`() {
|
||||
val view = track(ToolView(tool("g1", "grep", ToolExecState.COMPLETED).also { it.output = "match one\nmatch two" }))
|
||||
val req = view.headerPopup()
|
||||
assertNotNull(req)
|
||||
val body = req!!.build()
|
||||
try {
|
||||
val editors = popupEditors(body.component)
|
||||
editors.forEach { it.getEditor(true) }
|
||||
assertEquals(listOf("match one\nmatch two"), editors.map { it.text })
|
||||
assertTrue(body.component.preferredSize.height in 1..JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT))
|
||||
} finally {
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test tool header popup is absent when empty or expanded`() {
|
||||
val empty = track(ToolView(tool("g2", "grep", ToolExecState.COMPLETED)))
|
||||
assertNull(empty.headerPopup())
|
||||
|
||||
val view = track(ToolView(tool("g3", "grep", ToolExecState.COMPLETED).also { it.output = "hit" }))
|
||||
assertNotNull(view.headerPopup())
|
||||
view.toggle()
|
||||
assertNull(view.headerPopup())
|
||||
}
|
||||
|
||||
fun `test tool header popup disposes editor after hide and churn`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
val view = track(ToolView(tool("g4", "grep", ToolExecState.COMPLETED).also { it.output = "hit" }))
|
||||
repeat(20) {
|
||||
val body = view.headerPopup()!!.build()
|
||||
popupEditors(body.component).forEach { it.getEditor(true) }
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
// ---- helpers ------
|
||||
|
||||
private fun tool(id: String, name: String, state: ToolExecState, title: String? = null): Tool =
|
||||
|
||||
+38
@@ -10,11 +10,14 @@ import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
import ai.kilocode.rpc.dto.TodoViewDto
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JEditorPane
|
||||
import javax.swing.JPanel
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
@@ -185,5 +188,40 @@ class TodoWriteViewTest : BasePlatformTestCase() {
|
||||
return (header.layout as BorderLayout).hgap
|
||||
}
|
||||
|
||||
fun `test todo header popup shows when collapsed and lists todos`() {
|
||||
val view = TodoWriteView(tool("todowrite", ToolExecState.COMPLETED).also {
|
||||
it.todos = listOf(TodoDto("Done", "completed", "high"), TodoDto("Next", "pending", "medium"))
|
||||
})
|
||||
assertTrue(view.isExpanded())
|
||||
assertNull(view.headerPopup())
|
||||
|
||||
view.toggle()
|
||||
assertFalse(view.isExpanded())
|
||||
val body = view.headerPopup()!!.build()
|
||||
try {
|
||||
val html = popupHtml(body.component)
|
||||
assertTrue(html.contains("Done"))
|
||||
assertTrue(html.contains("Next"))
|
||||
} finally {
|
||||
Disposer.dispose(body.disposable)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test todo header popup is absent without todos`() {
|
||||
val view = TodoWriteView(tool("todowrite", ToolExecState.COMPLETED))
|
||||
view.toggle()
|
||||
assertNull(view.headerPopup())
|
||||
}
|
||||
|
||||
private fun popupHtml(root: JComponent): String {
|
||||
val out = StringBuilder()
|
||||
fun visit(component: JComponent) {
|
||||
if (component is JEditorPane) out.append(component.text)
|
||||
component.components.filterIsInstance<JComponent>().forEach(::visit)
|
||||
}
|
||||
visit(root)
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
private fun tool(name: String, state: ToolExecState) = Tool("p1", name, toolKind(name)).also { it.state = state }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user