fix(jetbrains): improve worktree PR tooltips

This commit is contained in:
kirillk
2026-08-07 17:53:42 -04:00
parent c8694bcffd
commit 89532bb897
17 changed files with 235 additions and 40 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show pull request titles and clearer multi-line badge tooltips in the Agent Manager worktree list.
@@ -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 }
@@ -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")
@@ -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<ActiveListBadge>
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) } },
)
@@ -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)
@@ -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("<br>"))
}
@@ -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
}
}
@@ -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. */
@@ -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. */
@@ -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
@@ -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=<html>{0} commits ahead, {1} behind, +{2} -{3}.<br>Click to open the diff against the base branch.</html>
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
@@ -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 <login> 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 <login> bug", row.title)
val tip = row.metrics?.prTooltip ?: error("expected PR tooltip")
assertEquals("<html>Draft #7 Fix &lt;login&gt; bug<br>(Feature Label)<br>Click to open the pull request in your browser.</html>", 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("<html>Open #8<br>Click to open the pull request in your browser.</html>", 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() }
}
@@ -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)
@@ -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)
}
@@ -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") { _, _ -> }
@@ -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))
}
}
@@ -37,6 +37,7 @@ data class WorktreePrDto(
val number: Int,
val state: GhState,
val url: String,
val title: String = "",
)
@Serializable