From 686208eb040318dfb3ce01d4cfe03a6999d4bab8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 11 Aug 2026 11:04:02 -0400 Subject: [PATCH] fix(jetbrains): align worktree editor titles --- .../client/agentManager/AgentManagerPanel.kt | 17 +++++++++----- .../worktree/WorktreeNameCache.kt | 23 ++++++++++++++++++- .../worktree/WorktreePrHeaderView.kt | 18 ++++----------- .../worktree/WorktreeSessionEditorKind.kt | 7 +----- .../agentManager/worktree/WorktreeTitle.kt | 20 ++++++++++++++++ .../WorktreeSessionEditorKindTest.kt | 10 ++++++++ .../worktree/WorktreeNameCacheTest.kt | 15 ++++++++++++ .../worktree/WorktreePrHeaderViewTest.kt | 5 ++-- 8 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeTitle.kt 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 d9d6513732d..d9c3c2f5f46 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.WorktreeNameCache 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.WorktreeTitle import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind import ai.kilocode.client.agentManager.worktree.prTooltip import ai.kilocode.client.agentManager.worktree.normalizeWorktreePath @@ -123,7 +124,10 @@ class AgentManagerPanel( bindStatus() bindEditorSelection() // Reflect names adopted or renamed in a worktree session editor tab in the list live. - service().addListener(this) { path, name -> controller.applyName(path, name) } + service().addListener(this) { path, name -> + controller.applyName(path, name) + project?.service()?.updatePresentation(WorktreeSessionEditorKind.ID, mapOf("path" to path)) + } ActionManager.getInstance().getAction("RenameElement")?.shortcutSet?.let { set -> edit.registerCustomShortcutSet(set, list, this) } @@ -331,7 +335,9 @@ class AgentManagerPanel( (0 until controller.model.size).map { val item = controller.model.getElementAt(it) val key = normalizeWorktreePath(item.path) - WorktreeRow(item, controller.isPending(item.id), controller.isDeleting(item.id), controller.kind(item.path), stats[key], prs[key]) + val pull = prs[key] + service().putPr(item.path, pull) + WorktreeRow(item, controller.isPending(item.id), controller.isDeleting(item.id), controller.kind(item.path), stats[key], pull) }, ActiveListSelection.PreserveNoScroll, ) @@ -442,13 +448,12 @@ class AgentManagerPanel( val pr: WorktreePrDto?, ) : ActiveListItem { override val key: String get() = dto.id - override val title: String get() = pr?.title?.trim()?.takeIf { it.isNotBlank() } ?: dto.name - override val description: String get() = defaultName + override val title: String get() = WorktreeTitle.text(dto.name, dto.path, pr) + override val description: String get() = WorktreeTitle.fallback(dto.path) 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 } + private val customName: String? get() = WorktreeTitle.custom(dto.name, dto.path) override val badges: List get() { if (pending || deleting) return emptyList() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCache.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCache.kt index 2fe40ddd38a..36ccb55055b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCache.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCache.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.rpc.dto.WorktreeDto +import ai.kilocode.rpc.dto.WorktreePrDto import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.util.Disposer @@ -14,11 +15,15 @@ import com.intellij.util.concurrency.annotations.RequiresEdt @Service(Service.Level.APP) class WorktreeNameCache { private val names = linkedMapOf() + private val pulls = linkedMapOf() private val listeners = mutableListOf<(String, String?) -> Unit>() @RequiresEdt fun get(path: String): String? = names[path] + @RequiresEdt + fun title(path: String): String = WorktreeTitle.text(names[path], path, pulls[path]) + @RequiresEdt fun put(path: String, name: String) { if (names[path] == name) return @@ -29,14 +34,30 @@ class WorktreeNameCache { @RequiresEdt fun put(item: WorktreeDto) = put(item.path, item.name) + @RequiresEdt + fun putPr(path: String, pull: WorktreePrDto?) { + val prev = pulls[path] + if (pull == null) { + if (prev == null) return + pulls.remove(path) + fire(path, names[path]) + return + } + if (prev == pull) return + pulls[path] = pull + fire(path, names[path]) + } + @RequiresEdt fun remove(path: String) { - if (names.remove(path) != null) fire(path, null) + val changed = names.remove(path) != null || pulls.remove(path) != null + if (changed) fire(path, null) } @RequiresEdt fun clear() { names.clear() + pulls.clear() } /** diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderView.kt index b1e9c1de9a1..913fd51a1ec 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderView.kt @@ -30,22 +30,24 @@ internal class WorktreePrHeaderView( private val changes = WorktreeStatsView(openDiff) private val statusPane = status.align(HAlign.LEFT, VAlign.CENTER) private val changesPane = changes.align(HAlign.RIGHT, VAlign.CENTER) as JComponent + private val actions = BorderLayoutPanel() private var pull: WorktreePrDto? = null private var state: GhState? = null private var text: String? = null private var tip: String? = null private var url: String? = null - private var changesLeft = false init { isOpaque = false + actions.isOpaque = false status.border = JBUI.Borders.empty(0, UiStyle.Gap.md(), 0, UiStyle.Gap.xs()) title.border = JBUI.Borders.empty(0, UiStyle.Gap.sm()) title.isOpaque = false changesPane.border = JBUI.Borders.emptyRight(UiStyle.Gap.pad()) + actions.addToRight(changesPane) addToLeft(statusPane) addToCenter(title) - addToRight(changesPane) + addToRight(actions) val listener = object : MouseAdapter() { override fun mouseClicked(event: MouseEvent) { url?.let(BrowserUtil::browse) @@ -93,23 +95,11 @@ internal class WorktreePrHeaderView( if (value) { if (statusPane.parent !== this) add(statusPane, BorderLayout.WEST) if (title.parent !== this) add(title, BorderLayout.CENTER) - placeChanges(left = false) title.isVisible = true return } if (statusPane.parent === this) remove(statusPane) - placeChanges(left = true) title.isVisible = false - } - - private fun placeChanges(left: Boolean) { - val constraint = if (left) BorderLayout.WEST else BorderLayout.EAST - val border = if (left) JBUI.Borders.emptyLeft(UiStyle.Gap.md()) else JBUI.Borders.emptyRight(UiStyle.Gap.pad()) - if (changesPane.parent === this && changesLeft == left) return - if (changesPane.parent === this) remove(changesPane) - changesLeft = left - changesPane.border = border - add(changesPane, constraint) changed() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorKind.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorKind.kt index 14869f7bcea..9692044292a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorKind.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorKind.kt @@ -25,7 +25,7 @@ object WorktreeSessionEditorKind : KiloEditorKind { override val id: String = ID override fun title(params: Map): String = params[PATH]?.let { path -> - service().get(path) ?: name(path) + service().title(path) } ?: KiloBundle.message("worktree.session.title") override fun icon(params: Map): Icon = WorktreeIcons.branch override fun fileType(params: Map): FileType = WorktreeSessionFileType @@ -46,11 +46,6 @@ object WorktreeSessionEditorKind : KiloEditorKind { return WorktreeSessionEditorPanel(parent, manager, controller, worktree, project) } - private fun name(path: String): String { - val value = path.trimEnd('/', '\\') - return value.substringAfterLast('/').substringAfterLast('\\').takeIf { it.isNotBlank() } ?: value - } - private const val PATH = "path" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeTitle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeTitle.kt new file mode 100644 index 00000000000..1e2d3f2e2b2 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeTitle.kt @@ -0,0 +1,20 @@ +package ai.kilocode.client.agentManager.worktree + +import ai.kilocode.rpc.dto.WorktreePrDto + +internal object WorktreeTitle { + fun text(name: String?, path: String, pull: WorktreePrDto? = null): String { + return pull?.title?.trim()?.takeIf { it.isNotBlank() } + ?: name?.takeIf { it.isNotBlank() } + ?: fallback(path) + } + + fun custom(name: String?, path: String): String? { + return name?.takeIf { it != fallback(path) } + } + + fun fallback(path: String): String { + val value = path.trimEnd('/', '\\') + return value.substringAfterLast('/').substringAfterLast('\\').takeIf { it.isNotBlank() } ?: value + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/WorktreeSessionEditorKindTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/WorktreeSessionEditorKindTest.kt index a91866fb260..1f6bbf7ec33 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/WorktreeSessionEditorKindTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/WorktreeSessionEditorKindTest.kt @@ -10,6 +10,8 @@ import ai.kilocode.client.vfs.KiloEditorKindRegistry import ai.kilocode.client.vfs.KiloPath import ai.kilocode.client.vfs.KiloVirtualFileKindRegistry import ai.kilocode.client.vfs.KiloVirtualFileSystem +import ai.kilocode.rpc.dto.GhState +import ai.kilocode.rpc.dto.WorktreePrDto import ai.kilocode.rpc.dto.WorktreeDto import com.intellij.openapi.components.service import com.intellij.openapi.vfs.VirtualFilePathWrapper @@ -58,4 +60,12 @@ class WorktreeSessionEditorKindTest : BasePlatformTestCase() { assertEquals("Feature Label", WorktreeSessionEditorKind.title(mapOf("path" to path))) } + + fun `test worktree session title uses same winning label as worktree list`() { + val path = "/repo/.kilo/worktrees/feature-x" + service().put(path, "Feature Label") + service().putPr(path, WorktreePrDto(path, 3, GhState.OPEN, "https://example.test/pr/3", "PR Label")) + + assertEquals("PR Label", WorktreeSessionEditorKind.title(mapOf("path" to path))) + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt index a6f709d8a62..a503105bbb0 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt @@ -1,7 +1,9 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.client.util.edtWait +import ai.kilocode.rpc.dto.GhState import ai.kilocode.rpc.dto.WorktreeDto +import ai.kilocode.rpc.dto.WorktreePrDto import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.util.Disposer @@ -43,6 +45,19 @@ class WorktreeNameCacheTest : BasePlatformTestCase() { assertTrue(events.isEmpty()) } + fun `test title prefers PR title then worktree name`() = edt { + val path = "/repo/wt" + cache().put(path, "Feature") + + assertEquals("Feature", cache().title(path)) + + cache().putPr(path, WorktreePrDto(path, 12, GhState.OPEN, "https://example.test/pr/12", "PR Title")) + assertEquals("PR Title", cache().title(path)) + + cache().putPr(path, WorktreePrDto(path, 12, GhState.OPEN, "https://example.test/pr/12", " ")) + assertEquals("Feature", cache().title(path)) + } + fun `test listener is removed with parent disposable`() = edt { val parent = Disposer.newDisposable("worktree-cache-listener") Disposer.register(testRootDisposable, parent) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderViewTest.kt index 0c7a2b0bdf5..0ca9990a847 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreePrHeaderViewTest.kt @@ -16,6 +16,7 @@ import java.awt.Point import java.awt.event.InputEvent import java.awt.event.MouseEvent import javax.swing.JComponent +import javax.swing.SwingUtilities class WorktreePrHeaderViewTest : BasePlatformTestCase() { fun `test PR state title and colors render`() { @@ -61,7 +62,7 @@ class WorktreePrHeaderViewTest : BasePlatformTestCase() { assertEquals(Cursor.DEFAULT_CURSOR, title.cursor.type) } - fun `test no PR aligns changes badge to the left`() { + fun `test no PR keeps changes badge on the right`() { val view = edt { WorktreePrHeaderView {} } val changes = edt { UIUtil.findComponentOfType(view, WorktreeStatsView::class.java)!! } @@ -71,7 +72,7 @@ class WorktreePrHeaderViewTest : BasePlatformTestCase() { view.doLayout() } - assertTrue(edt { changes.parent.x < 20 }) + assertTrue(edt { SwingUtilities.convertPoint(changes.parent, Point(0, 0), view).x > 300 }) } fun `test changes view visibility follows stats`() {