From 89532bb897102ca64369a67dd62513e72d92828b Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 7 Aug 2026 17:53:42 -0400 Subject: [PATCH] fix(jetbrains): improve worktree PR tooltips --- .changeset/jetbrains-pr-worktree-tooltips.md | 5 ++ .../backend/rpc/KiloWorktreeRpcApiImpl.kt | 26 ++++---- .../backend/rpc/KiloWorktreeRpcApiImplTest.kt | 14 +++++ .../client/agentManager/AgentManagerPanel.kt | 8 ++- .../agentManager/worktree/WorktreeActivity.kt | 9 +-- .../worktree/WorktreeStatsView.kt | 43 ++++++++++--- .../client/session/SessionActivityKind.kt | 2 +- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 60 +++++++++++++++++++ .../client/ui/list/ActiveListModel.kt | 1 + .../client/ui/list/ActiveListRenderer.kt | 4 +- .../resources/messages/KiloBundle.properties | 8 ++- .../agentManager/AgentManagerPanelTest.kt | 53 ++++++++++++++++ .../worktree/WorktreeActivityTest.kt | 5 +- .../WorktreeSessionEditorPanelTest.kt | 3 +- .../settings/base/SettingsListViewTest.kt | 15 +++++ .../ai/kilocode/client/ui/UiStyleTest.kt | 18 ++++++ .../kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt | 1 + 17 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 .changeset/jetbrains-pr-worktree-tooltips.md diff --git a/.changeset/jetbrains-pr-worktree-tooltips.md b/.changeset/jetbrains-pr-worktree-tooltips.md new file mode 100644 index 00000000000..cd7d8e5931f --- /dev/null +++ b/.changeset/jetbrains-pr-worktree-tooltips.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show pull request titles and clearer multi-line badge tooltips in the Agent Manager worktree list. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt index 2d543fa331a..cbf9efda7f7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt @@ -96,7 +96,7 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi { var status = GhAvailability.OK val data = parallel(items) { item -> if (status != GhAvailability.OK) return@parallel null - val out = runGh(Path.of(item.path).normalize(), "pr", "view", item.branch, "--json", "number,state,isDraft,url") + val out = runGh(Path.of(item.path).normalize(), "pr", "view", item.branch, "--json", "number,state,isDraft,url,title") if (!out.ok) { when (prError(out.stderr)) { GhAvailability.UNAUTH -> status = GhAvailability.UNAUTH @@ -318,18 +318,20 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi { return GhAvailability.OK } - private fun parsePr(path: String, raw: String): WorktreePrDto? { - val obj = runCatching { json.parseToJsonElement(raw) as? JsonObject }.getOrNull() ?: return null - val number = obj["number"]?.jsonPrimitive?.intOrNull ?: return null - val url = obj["url"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } ?: return null - val draft = obj["isDraft"]?.jsonPrimitive?.booleanOrNull == true - val state = if (draft) GhState.DRAFT else when (obj["state"]?.jsonPrimitive?.content?.uppercase()) { - "MERGED" -> GhState.MERGED - "CLOSED" -> GhState.CLOSED - else -> GhState.OPEN - } - return WorktreePrDto(path, number, state, url) +} + +internal fun parsePr(path: String, raw: String): WorktreePrDto? { + val obj = runCatching { json.parseToJsonElement(raw) as? JsonObject }.getOrNull() ?: return null + val number = obj["number"]?.jsonPrimitive?.intOrNull ?: return null + val url = obj["url"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } ?: return null + val title = obj["title"]?.jsonPrimitive?.content?.trim().orEmpty() + val draft = obj["isDraft"]?.jsonPrimitive?.booleanOrNull == true + val state = if (draft) GhState.DRAFT else when (obj["state"]?.jsonPrimitive?.content?.uppercase()) { + "MERGED" -> GhState.MERGED + "CLOSED" -> GhState.CLOSED + else -> GhState.OPEN } + return WorktreePrDto(path, number, state, url, title) } private val json = Json { prettyPrint = true; ignoreUnknownKeys = true } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt index 9552fbfeece..c59e87b1b5d 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt @@ -1,6 +1,7 @@ package ai.kilocode.backend.rpc import ai.kilocode.rpc.dto.CreateWorktreeRequestDto +import ai.kilocode.rpc.dto.GhState import ai.kilocode.rpc.dto.WorktreeDto import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.CapturingProcessHandler @@ -337,6 +338,19 @@ class KiloWorktreeRpcApiImplTest { assertEquals(0, item.behind) } + @Test + fun `parsePr reads title from gh output`() { + val pull = assertNotNull(parsePr("/repo/.kilo/worktrees/feature-x", """ + {"number":12,"state":"OPEN","isDraft":false,"url":"https://example.test/pr/12","title":" Fix login bug "} + """.trimIndent())) + + assertEquals("/repo/.kilo/worktrees/feature-x", pull.path) + assertEquals(12, pull.number) + assertEquals(GhState.OPEN, pull.state) + assertEquals("https://example.test/pr/12", pull.url) + assertEquals("Fix login bug", pull.title) + } + private fun initRepo() { git(repo, "init") git(repo, "config", "user.email", "test@kilo.ai") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt index e09f70992bf..6d4273f7e7d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt @@ -11,6 +11,7 @@ import ai.kilocode.client.agentManager.worktree.WorktreeEditorMatchers import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorMatcher import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorKind import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind +import ai.kilocode.client.agentManager.worktree.prTooltip import ai.kilocode.client.agentManager.worktree.normalizeWorktreePath import ai.kilocode.client.agentManager.worktree.style import ai.kilocode.client.agentManager.worktree.worktreeActivityBadge @@ -481,11 +482,13 @@ class AgentManagerPanel( val pr: WorktreePrDto?, ) : ActiveListItem { override val key: String get() = dto.id - override val title: String get() = dto.name - override val description: String get() = dto.path.trimEnd('/').substringAfterLast('/') + override val title: String get() = pr?.title?.trim()?.takeIf { it.isNotBlank() } ?: dto.name + override val description: String get() = defaultName override val tooltip: String? get() = null override val icon = WorktreeIcons.forRow(dto.locked, pending) override val search: String get() = listOfNotNull(dto.name, dto.branch, dto.path, dto.lockReason).joinToString(" ") + private val defaultName: String get() = dto.path.trimEnd('/').substringAfterLast('/') + private val customName: String? get() = dto.name.takeIf { it != defaultName } override val badges: List get() { if (pending || deleting) return emptyList() @@ -503,6 +506,7 @@ class AgentManagerPanel( ahead = s?.ahead ?: 0, behind = s?.behind ?: 0, pr = p?.let { ActiveListBadge("#${it.number}", style(it.state)) }, + prTooltip = p?.let { prTooltip(it, customName) }, onChanges = s?.let { { openBranchDiff(dto.path) } }, onPr = p?.url?.let { url -> { BrowserUtil.browse(url) } }, ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivity.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivity.kt index 236a5bc0e36..0e561668a34 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivity.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivity.kt @@ -1,7 +1,6 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.client.session.SessionActivityKind -import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.rpc.dto.SessionActivityDto import ai.kilocode.rpc.dto.SessionActivityKindDto @@ -12,14 +11,8 @@ internal fun aggregateWorktreeActivity( .groupBy { normalize(it.directory) } .mapValues { (_, items) -> items.map { kind(it.kind) }.minBy(::rank) } -/** - * The worktree lists mute the informational [SessionActivityKind.RUNNING] state into the same - * subtle pill Settings uses for its "built-in" badge, so only the actionable states (question / - * permission) keep the prominent primary styling. - */ internal fun worktreeActivityBadge(kind: SessionActivityKind): ActiveListBadge { - val style = if (kind == SessionActivityKind.RUNNING) UiStyle.Badge.Secondary else kind.style() - return ActiveListBadge(kind.label(), style) + return ActiveListBadge(kind.label(), kind.style()) } internal fun normalizeWorktreePath(path: String): String = normalize(path) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatsView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatsView.kt index 46ac169c843..ab1bee72ec8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatsView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatsView.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.util.IconLoader import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI +import com.intellij.xml.util.XmlStringUtil import java.awt.BorderLayout import java.awt.Cursor import java.awt.Dimension @@ -83,14 +84,14 @@ internal class WorktreeStatsView( if (this.stats == stats && this.pull == pull) return this.stats = stats this.pull = pull - sync(stats, pull?.let { ActiveListBadge("#${it.number}", style(it.state)) }, pull?.url, pull?.let { KiloBundle.message("worktree.pr.tooltip", it.number, it.state.name.lowercase()) }) + sync(stats, pull?.let { ActiveListBadge("#${it.number}", style(it.state)) }, pull?.url, pull?.let(::prTooltip)) } - fun update(stats: WorktreeStatsDto?, badge: ActiveListBadge?) { - if (this.stats == stats && pull == null && (pr.icon as? FilledBadgeIcon)?.text == badge?.text) return + fun update(stats: WorktreeStatsDto?, badge: ActiveListBadge?, prTip: String? = badge?.text) { + if (this.stats == stats && pull == null && (pr.icon as? FilledBadgeIcon)?.text == badge?.text && prHit.tip == prTip) return this.stats = stats this.pull = null - sync(stats, badge, null, badge?.text) + sync(stats, badge, null, prTip) } private fun sync(stats: WorktreeStatsDto?, badge: ActiveListBadge?, link: String?, tip: String?) { @@ -172,8 +173,34 @@ internal class WorktreeStatsView( } internal fun style(state: GhState): UiStyle.Badge.Style = when (state) { - GhState.OPEN -> UiStyle.Badge.Primary - GhState.DRAFT -> UiStyle.Badge.Secondary - GhState.MERGED -> UiStyle.Badge.Highlight - GhState.CLOSED -> UiStyle.Badge.Alert + GhState.OPEN -> UiStyle.Badge.PullRequestOpen + GhState.DRAFT -> UiStyle.Badge.PullRequestDraft + GhState.MERGED -> UiStyle.Badge.PullRequestMerged + GhState.CLOSED -> UiStyle.Badge.PullRequestClosed +} + +internal fun stateLabel(state: GhState): String = when (state) { + GhState.OPEN -> KiloBundle.message("worktree.pr.state.open") + GhState.DRAFT -> KiloBundle.message("worktree.pr.state.draft") + GhState.MERGED -> KiloBundle.message("worktree.pr.state.merged") + GhState.CLOSED -> KiloBundle.message("worktree.pr.state.closed") +} + +internal fun prTooltip(pull: WorktreePrDto, name: String? = null): String { + val title = pull.title.trim() + val head = buildString { + append(stateLabel(pull.state)) + append(" #") + append(pull.number) + if (title.isNotBlank()) { + append(' ') + append(title) + } + } + val lines = listOfNotNull( + head, + name?.takeIf { title.isNotBlank() }?.let { "($it)" }, + KiloBundle.message("worktree.pr.tooltip.open"), + ).map(XmlStringUtil::escapeString) + return XmlStringUtil.wrapInHtml(lines.joinToString("
")) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt index 5d1ee27ce51..30869a4db22 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt @@ -20,7 +20,7 @@ enum class SessionActivityKind { } fun style(): UiStyle.Badge.Style = when (this) { - RUNNING -> UiStyle.Badge.Alert + RUNNING -> UiStyle.Badge.SessionRunning LOGIN_REQUIRED, PERMISSION, PLAN, QUESTION -> UiStyle.Badge.Primary } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 7e711204661..5fa6d030444 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -105,6 +105,66 @@ object UiStyle { JBColor(Color.BLACK, Color.WHITE), ) } + + object SessionRunning : Style { + override fun bg(): Color = JBColor.namedColor( + "Kilo.SessionStatus.runningBadgeBackground", + JBColor(Color(0xF9, 0x73, 0x16), Color(0xC2, 0x41, 0x0C)), + ) + + override fun fg(): Color = JBColor.namedColor( + "Kilo.SessionStatus.runningBadgeForeground", + Color.WHITE, + ) + } + + object PullRequestOpen : Style { + override fun bg(): Color = JBColor.namedColor( + "Kilo.PullRequest.openBadgeBackground", + JBColor(Color(0x1F, 0x88, 0x3D), Color(0x23, 0x86, 0x36)), + ) + + override fun fg(): Color = JBColor.namedColor( + "Kilo.PullRequest.openBadgeForeground", + Color.WHITE, + ) + } + + object PullRequestDraft : Style { + override fun bg(): Color = JBColor.namedColor( + "Kilo.PullRequest.draftBadgeBackground", + JBColor(Color(0x6E, 0x77, 0x81), Color(0x6E, 0x76, 0x81)), + ) + + override fun fg(): Color = JBColor.namedColor( + "Kilo.PullRequest.draftBadgeForeground", + Color.WHITE, + ) + } + + object PullRequestMerged : Style { + override fun bg(): Color = JBColor.namedColor( + "Kilo.PullRequest.mergedBadgeBackground", + JBColor(Color(0x82, 0x50, 0xDF), Color(0x89, 0x57, 0xE5)), + ) + + override fun fg(): Color = JBColor.namedColor( + "Kilo.PullRequest.mergedBadgeForeground", + Color.WHITE, + ) + } + + object PullRequestClosed : Style { + override fun bg(): Color = JBColor.namedColor( + "Kilo.PullRequest.closedBadgeBackground", + JBColor(Color(0xCF, 0x22, 0x2E), Color(0xDA, 0x36, 0x33)), + ) + + override fun fg(): Color = JBColor.namedColor( + "Kilo.PullRequest.closedBadgeForeground", + Color.WHITE, + ) + } } /** Theme-aware colors and color math used by multiple UI surfaces. */ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt index 1d6deb71d2a..edf5d375e04 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListModel.kt @@ -23,6 +23,7 @@ internal data class ActiveListMetrics( val ahead: Int = 0, val behind: Int = 0, val pr: ActiveListBadge? = null, + val prTooltip: String? = null, /** Click handler for the changes badge, e.g. open the branch diff. Null leaves it inert. */ val onChanges: (() -> Unit)? = null, /** Click handler for the PR badge, e.g. open the pull request. Null leaves it inert. */ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt index 6b9a14eca8c..1865f24cc0e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt @@ -85,7 +85,7 @@ internal class ActiveListRenderer( // instead of drifting to the far right of the row. private val header = titleGroup.align(HAlign.LEFT, VAlign.CENTER) private val desc = JBLabel() - private val metrics = WorktreeStatsView(fill = false) + private val metrics = WorktreeStatsView() private val metricsPane = metrics.align(HAlign.RIGHT, VAlign.CENTER) // The description (branch) line carries the changes/PR metrics on its trailing edge so they sit // on the branch row instead of spanning the full row height. @@ -216,7 +216,7 @@ internal class ActiveListRenderer( } desc.foreground = weak val data = if (value.deleting) null else value.metrics - metrics.update(data?.let { WorktreeStatsDto("", it.additions, it.deletions, it.ahead, it.behind) }, data?.pr) + metrics.update(data?.let { WorktreeStatsDto("", it.additions, it.deletions, it.ahead, it.behind) }, data?.pr, data?.prTooltip ?: data?.pr?.text) metrics.setActions(data?.onChanges, data?.onPr) val end = if (value.deleting) KiloBundle.message("common.deleting") else value.trailing.orEmpty() trail.text = end diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 5312aebca82..cdfe211a04f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -369,8 +369,12 @@ worktree.configure.branch.required=Branch name is required worktree.stats.diff.tooltip={0} additions, {1} deletions worktree.stats.ahead.tooltip=Commits ahead of base branch worktree.stats.behind.tooltip=Commits behind base branch -worktree.stats.tooltip={0} commits ahead, {1} behind, +{2} -{3}. Click to open the diff against the base branch. -worktree.pr.tooltip=Pull request #{0} ({1}). Click to open it in your browser. +worktree.stats.tooltip={0} commits ahead, {1} behind, +{2} -{3}.
Click to open the diff against the base branch. +worktree.pr.state.open=Open +worktree.pr.state.draft=Draft +worktree.pr.state.merged=Merged +worktree.pr.state.closed=Closed +worktree.pr.tooltip.open=Click to open the pull request in your browser. worktree.gh.missing.title=GitHub CLI not found worktree.gh.missing.content=Install gh to show pull request badges for worktrees. worktree.gh.install=Install diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt index 415c7f02131..0bdaa41b316 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt @@ -18,6 +18,8 @@ import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListItem import ai.kilocode.client.ui.list.ActiveListMetrics +import ai.kilocode.client.ui.list.ACTIVE_LIST_PR_CELL +import ai.kilocode.client.ui.list.activeListCellBounds import ai.kilocode.client.ui.list.activeListToolWindowBackground import ai.kilocode.client.vfs.KiloPath import ai.kilocode.client.vfs.KiloVfsManager @@ -43,6 +45,7 @@ import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane import com.intellij.util.ui.UIUtil import java.awt.event.MouseEvent +import java.awt.Point import javax.swing.JComponent import javax.swing.SwingUtilities import kotlinx.coroutines.CompletableDeferred @@ -454,6 +457,54 @@ class AgentManagerPanelTest : BasePlatformTestCase() { assertFalse(edt { panel.canOpenPr(null) }) } + fun `test pr title replaces row name and tooltip reveals custom name`() { + val path = "${project.basePath!!}/.kilo/worktrees/feature-x" + val item = WorktreeDto(path, "Feature Label", "feature/x", path) + rpc.listed += item + rpc.prResult = WorktreePrListDto(GhAvailability.OK, listOf(WorktreePrDto(path, 7, GhState.DRAFT, "https://example.test/pr/7", "Fix bug"))) + val timers = TestUiTimers() + ApplicationManager.getApplication().replaceService(KiloWorktreeService::class.java, service, testRootDisposable) + project.replaceService(WorktreeStatusService::class.java, WorktreeStatusService(project, coroutines.scope, timers), testRootDisposable) + val controller = WorktreeController(service, project.basePath!!, coroutines.scope) + val panel = edt { AgentManagerPanel(testRootDisposable, controller, project) } + edt { controller.reload() } + timers.advanceBy(300) + flush() + + val row = row(panel, 0) + assertEquals("Fix bug", row.title) + val tip = row.metrics?.prTooltip ?: error("expected PR tooltip") + assertEquals("Draft #7 Fix <login> bug
(Feature Label)
Click to open the pull request in your browser.", tip) + val list = edt { UIUtil.findComponentOfType(panel, JBList::class.java)!! } + edt { + list.size = java.awt.Dimension(360, 80) + list.doLayout() + UIUtil.dispatchAllInvocationEvents() + } + val area = edt { activeListCellBounds(list, 0, selected = false).getValue(ACTIVE_LIST_PR_CELL) } + + assertEquals(tip, edt { list.getToolTipText(MouseEvent(list, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, center(area).x, center(area).y, 0, false)) }) + } + + fun `test blank pr title keeps row name and omits custom name line`() { + val path = "${project.basePath!!}/.kilo/worktrees/feature-x" + val item = WorktreeDto(path, "Feature Label", "feature/x", path) + rpc.listed += item + rpc.prResult = WorktreePrListDto(GhAvailability.OK, listOf(WorktreePrDto(path, 8, GhState.OPEN, "https://example.test/pr/8", " "))) + val timers = TestUiTimers() + ApplicationManager.getApplication().replaceService(KiloWorktreeService::class.java, service, testRootDisposable) + project.replaceService(WorktreeStatusService::class.java, WorktreeStatusService(project, coroutines.scope, timers), testRootDisposable) + val controller = WorktreeController(service, project.basePath!!, coroutines.scope) + val panel = edt { AgentManagerPanel(testRootDisposable, controller, project) } + edt { controller.reload() } + timers.advanceBy(300) + flush() + + val row = row(panel, 0) + assertEquals("Feature Label", row.title) + assertEquals("Open #8
Click to open the pull request in your browser.", row.metrics?.prTooltip) + } + fun `test worktree row hides badge while pending or deleting`() { val path = "feature/y" val activity = MutableStateFlow(mapOf( @@ -482,6 +533,8 @@ class AgentManagerPanelTest : BasePlatformTestCase() { return edt { list.model.getElementAt(idx) as ActiveListItem } } + private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2) + private fun pump() { ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivityTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivityTest.kt index 1c087f88dbe..d5b365b9551 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivityTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeActivityTest.kt @@ -1,7 +1,6 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.client.session.SessionActivityKind -import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.SessionActivityDto import ai.kilocode.rpc.dto.SessionActivityKindDto import kotlin.test.Test @@ -43,8 +42,8 @@ class WorktreeActivityTest { } @Test - fun `running badge uses the subtle secondary pill while actionable kinds stay prominent`() { - assertEquals(UiStyle.Badge.Secondary, worktreeActivityBadge(SessionActivityKind.RUNNING).style) + fun `worktree activity badges use shared session activity styles`() { + assertEquals(SessionActivityKind.RUNNING.style(), worktreeActivityBadge(SessionActivityKind.RUNNING).style) assertEquals(SessionActivityKind.QUESTION.style(), worktreeActivityBadge(SessionActivityKind.QUESTION).style) assertEquals(SessionActivityKind.PERMISSION.style(), worktreeActivityBadge(SessionActivityKind.PERMISSION).style) assertEquals(SessionActivityKind.RUNNING.label(), worktreeActivityBadge(SessionActivityKind.RUNNING).text) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt index 0dc6c720bf8..aa179915b0f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt @@ -12,7 +12,6 @@ import ai.kilocode.client.session.history.LocalHistoryItem import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.TestCoroutines import ai.kilocode.client.testing.fire -import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListItem import ai.kilocode.client.ui.list.activeListSectionTitle @@ -146,7 +145,7 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() { assertEquals("Session ses_1", row.title) assertNull(row.icon) assertNull(row.description) - assertEquals(listOf(ActiveListBadge(SessionActivityKind.RUNNING.label(), UiStyle.Badge.Secondary)), row.badges) + assertEquals(listOf(ActiveListBadge(SessionActivityKind.RUNNING.label(), SessionActivityKind.RUNNING.style())), row.badges) assertNull(row.trailing) assertEquals(HistoryTime.title(HistoryTime.section(LocalHistoryItem(session))), row.section) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index c83ecee8d43..1911a877e75 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -940,6 +940,7 @@ class SettingsListViewTest : BasePlatformTestCase() { UIUtil.dispatchAllInvocationEvents() val area = activeListCellBounds(view.list, 0, selected = true).getValue(ACTIVE_LIST_PR_CELL) + assertEquals("#12", view.list.getToolTipText(event(view.list, center(area)))) hover(view, center(area)) assertEquals(Cursor.HAND_CURSOR, view.list.cursor.type) @@ -948,6 +949,20 @@ class SettingsListViewTest : BasePlatformTestCase() { } } + fun `test pr badge uses custom tooltip when supplied`() { + edt { + val view = ActiveListView("Empty") { _, _ -> } + view.update(listOf(metricsItem("wt", "Alpha", ActiveListMetrics(pr = ActiveListBadge("#12"), prTooltip = "PR details")))) + view.list.size = Dimension(360, 80) + view.list.doLayout() + UIUtil.dispatchAllInvocationEvents() + + val area = activeListCellBounds(view.list, 0, selected = true).getValue(ACTIVE_LIST_PR_CELL) + + assertEquals("PR details", view.list.getToolTipText(event(view.list, center(area)))) + } + } + fun `test inert changes badge is not hit tested`() { edt { val view = ActiveListView("Empty") { _, _ -> } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index 384f53b9c21..e2710a9867a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -1,6 +1,9 @@ package ai.kilocode.client.ui +import ai.kilocode.client.agentManager.worktree.style +import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.rpc.dto.GhState import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.JBUI import java.awt.Color @@ -45,4 +48,19 @@ class UiStyleTest : BasePlatformTestCase() { assertTrue(SessionUiStyle.View.Tool.BODY_LINES > 0) assertEquals(5, SessionUiStyle.View.Reasoning.BODY_LINES) } + + fun `test session status badges use shared styles`() { + assertSame(UiStyle.Badge.SessionRunning, SessionActivityKind.RUNNING.style()) + assertSame(UiStyle.Badge.Primary, SessionActivityKind.QUESTION.style()) + assertSame(UiStyle.Badge.Primary, SessionActivityKind.PLAN.style()) + assertSame(UiStyle.Badge.Primary, SessionActivityKind.PERMISSION.style()) + assertSame(UiStyle.Badge.Primary, SessionActivityKind.LOGIN_REQUIRED.style()) + } + + fun `test pull request states use github badge styles`() { + assertSame(UiStyle.Badge.PullRequestOpen, style(GhState.OPEN)) + assertSame(UiStyle.Badge.PullRequestDraft, style(GhState.DRAFT)) + assertSame(UiStyle.Badge.PullRequestMerged, style(GhState.MERGED)) + assertSame(UiStyle.Badge.PullRequestClosed, style(GhState.CLOSED)) + } } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt index c1f4f0dabff..bb356f810e6 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt @@ -37,6 +37,7 @@ data class WorktreePrDto( val number: Int, val state: GhState, val url: String, + val title: String = "", ) @Serializable