mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): align worktree action badges
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.agentManager.SidePanelKeys
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeDataKeys
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
|
||||
class OpenWorktreeDiffAction : AnAction() {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val panel = e.getData(SidePanelKeys.WORKTREE_PANEL)
|
||||
val item = e.getData(WorktreeDataKeys.WORKTREE)
|
||||
e.presentation.isEnabledAndVisible = panel != null && panel.canOpenDiff(item)
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val panel = e.getData(SidePanelKeys.WORKTREE_PANEL) ?: return
|
||||
val item = e.getData(WorktreeDataKeys.WORKTREE) ?: return
|
||||
if (panel.canOpenDiff(item)) panel.openDiff(item)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.agentManager.SidePanelKeys
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeDataKeys
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
|
||||
class OpenWorktreePrAction : AnAction() {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val panel = e.getData(SidePanelKeys.WORKTREE_PANEL)
|
||||
val item = e.getData(WorktreeDataKeys.WORKTREE)
|
||||
e.presentation.isEnabledAndVisible = panel != null && panel.canOpenPr(item)
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val panel = e.getData(SidePanelKeys.WORKTREE_PANEL) ?: return
|
||||
val item = e.getData(WorktreeDataKeys.WORKTREE) ?: return
|
||||
if (panel.canOpenPr(item)) panel.openPr(item)
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -98,7 +98,7 @@ class AgentManagerPanel(
|
||||
},
|
||||
onSelect = { selectedRow()?.dto?.id?.let { selected = it } },
|
||||
menu = ActiveListMenu(WorktreeDataKeys.WORKTREE, group, element = { row ->
|
||||
(row as? WorktreeRow)?.dto?.takeIf { canRename(it) || canDelete(it) }
|
||||
(row as? WorktreeRow)?.dto?.takeIf { canRename(it) || canDelete(it) || canOpenPr(it) || canOpenDiff(it) }
|
||||
}),
|
||||
)
|
||||
private var selected: String? = null
|
||||
@@ -206,6 +206,26 @@ class AgentManagerPanel(
|
||||
|
||||
internal fun canDelete(item: WorktreeDto?): Boolean = deletable(item)
|
||||
|
||||
internal fun canOpenPr(item: WorktreeDto?): Boolean = prUrl(item) != null
|
||||
|
||||
internal fun openPr(item: WorktreeDto) = prUrl(item)?.let { BrowserUtil.browse(it) }
|
||||
|
||||
/** The PR URL for [item], or null when it has none or is not in a stable, openable state. */
|
||||
private fun prUrl(item: WorktreeDto?): String? {
|
||||
if (item == null || item.main) return null
|
||||
if (controller.isPending(item.id) || controller.isDeleting(item.id)) return null
|
||||
return prs[normalizeWorktreePath(item.path)]?.url
|
||||
}
|
||||
|
||||
internal fun canOpenDiff(item: WorktreeDto?): Boolean {
|
||||
if (item == null || item.main || project == null) return false
|
||||
return !controller.isPending(item.id) && !controller.isDeleting(item.id)
|
||||
}
|
||||
|
||||
internal fun openDiff(item: WorktreeDto) {
|
||||
if (canOpenDiff(item)) openBranchDiff(item.path)
|
||||
}
|
||||
|
||||
private fun showDeletePopup(item: WorktreeDto, cell: String? = null) {
|
||||
val opts = ActiveListDeleteOptions(
|
||||
message = KiloBundle.message("worktree.delete.confirm.message", item.name),
|
||||
|
||||
+11
-13
@@ -8,10 +8,7 @@ import ai.kilocode.client.ui.list.ACTIVE_LIST_CHANGES_CELL
|
||||
import ai.kilocode.client.ui.list.ACTIVE_LIST_PR_CELL
|
||||
import ai.kilocode.client.ui.list.ActiveListBadge
|
||||
import ai.kilocode.client.ui.list.ActiveListHitCell
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import ai.kilocode.rpc.dto.GhState
|
||||
import ai.kilocode.rpc.dto.WorktreePrDto
|
||||
import ai.kilocode.rpc.dto.WorktreeStatsDto
|
||||
@@ -47,10 +44,9 @@ internal class WorktreeStatsView(
|
||||
// ids back and routes the click. Standalone (toolbar) usage keeps its own listeners below.
|
||||
private val changeHit = HitRegion(ACTIVE_LIST_CHANGES_CELL).apply { add(change, BorderLayout.CENTER) }
|
||||
private val prHit = HitRegion(ACTIVE_LIST_PR_CELL).apply { add(pr, BorderLayout.CENTER) }
|
||||
// Change badge and PR link stack vertically, each pinned to the trailing edge.
|
||||
private val changeLine = changeHit.align(HAlign.RIGHT, VAlign.CENTER)
|
||||
private val prLine = prHit.align(HAlign.RIGHT, VAlign.CENTER)
|
||||
private val row = Stack.vertical(UiStyle.Gap.sm()).next(changeLine).next(prLine)
|
||||
// Change badge and PR link share one row: the changes trail first, the PR link is pinned last so
|
||||
// it is always the rightmost element.
|
||||
private val row = Stack.horizontal(UiStyle.Gap.md()).next(changeHit).next(prHit)
|
||||
private var url: String? = null
|
||||
private var stats: WorktreeStatsDto? = null
|
||||
private var pull: WorktreePrDto? = null
|
||||
@@ -59,7 +55,7 @@ internal class WorktreeStatsView(
|
||||
add(row)
|
||||
changeHit.act = openDiff
|
||||
prHit.act = { url?.let(BrowserUtil::browse) }
|
||||
diff.toolTipText = KiloBundle.message("worktree.stats.diff.tooltip", 0, 0)
|
||||
diff.toolTipText = KiloBundle.message("worktree.stats.tooltip", 0, 0, 0, 0)
|
||||
changeHit.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(event: MouseEvent) {
|
||||
changeHit.act?.invoke()
|
||||
@@ -107,18 +103,20 @@ internal class WorktreeStatsView(
|
||||
ahead.isVisible = s.ahead > 0
|
||||
diff.update(s.additions, s.deletions)
|
||||
diff.isVisible = s.additions > 0 || s.deletions > 0
|
||||
val diffTip = KiloBundle.message("worktree.stats.diff.tooltip", s.additions, s.deletions)
|
||||
diff.toolTipText = diffTip
|
||||
changeHit.tip = diffTip
|
||||
// One descriptive tooltip for the whole changes region: ahead/behind, the +/- diff, and what
|
||||
// clicking does.
|
||||
val changeTip = KiloBundle.message("worktree.stats.tooltip", s.ahead, s.behind, s.additions, s.deletions)
|
||||
diff.toolTipText = changeTip
|
||||
changeHit.tip = changeTip
|
||||
changeHit.toolTipText = changeTip
|
||||
url = link
|
||||
pr.icon = badge?.let { FilledBadgeIcon(it.text, it.style) }
|
||||
pr.toolTipText = tip
|
||||
prHit.tip = tip
|
||||
prHit.toolTipText = tip
|
||||
pr.isVisible = badge != null
|
||||
val changesVisible = behind.isVisible || ahead.isVisible || diff.isVisible
|
||||
changeLine.isVisible = changesVisible
|
||||
changeHit.isVisible = changesVisible
|
||||
prLine.isVisible = pr.isVisible
|
||||
prHit.isVisible = pr.isVisible
|
||||
isVisible = changesVisible || pr.isVisible
|
||||
applyCursors()
|
||||
|
||||
+17
-4
@@ -85,14 +85,20 @@ 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 text = Stack.vertical().next(header).next(desc)
|
||||
private val metrics = WorktreeStatsView(fill = false)
|
||||
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.
|
||||
private val descLine = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply {
|
||||
add(desc, BorderLayout.CENTER)
|
||||
add(metricsPane, BorderLayout.EAST)
|
||||
}
|
||||
private val text = Stack.vertical().next(header).next(descLine)
|
||||
private val textPane = text.align(HAlign.TRACK, VAlign.CENTER)
|
||||
private val trail = JBLabel().apply { horizontalAlignment = SwingConstants.RIGHT }
|
||||
private val metrics = WorktreeStatsView(fill = false)
|
||||
private val trailPane = trail.align(HAlign.RIGHT, VAlign.CENTER)
|
||||
private val endPane = JPanel(BorderLayout()).apply {
|
||||
add(trailPane, BorderLayout.CENTER)
|
||||
add(metrics, BorderLayout.EAST)
|
||||
}
|
||||
private val cells = Stack.horizontal(activeListCellGap())
|
||||
private val cellPane = cells.align(HAlign.RIGHT, VAlign.CENTER)
|
||||
@@ -135,8 +141,10 @@ internal class ActiveListRenderer(
|
||||
text,
|
||||
textPane,
|
||||
desc,
|
||||
trail,
|
||||
descLine,
|
||||
metrics,
|
||||
metricsPane,
|
||||
trail,
|
||||
trailPane,
|
||||
endPane,
|
||||
cells,
|
||||
@@ -214,6 +222,11 @@ internal class ActiveListRenderer(
|
||||
trail.text = end
|
||||
trail.isVisible = end.isNotBlank() && data == null
|
||||
metrics.isVisible = data != null && !value.deleting
|
||||
// Hide the wrapper too so a metrics-less row does not reserve the trailing gap on its
|
||||
// description, and collapse the whole second row when it would be empty so title-only rows
|
||||
// stay vertically centered.
|
||||
metricsPane.isVisible = metrics.isVisible
|
||||
descLine.isVisible = desc.isVisible || metrics.isVisible
|
||||
trail.foreground = weak
|
||||
|
||||
val hovered = (list as? ActiveListActive)?.hoveredIndex() == index
|
||||
|
||||
@@ -241,6 +241,12 @@
|
||||
<action id="Kilo.Worktree.Delete"
|
||||
class="ai.kilocode.client.actions.DeleteWorktreeAction"/>
|
||||
|
||||
<action id="Kilo.Worktree.OpenPr"
|
||||
class="ai.kilocode.client.actions.OpenWorktreePrAction"/>
|
||||
|
||||
<action id="Kilo.Worktree.OpenDiff"
|
||||
class="ai.kilocode.client.actions.OpenWorktreeDiffAction"/>
|
||||
|
||||
<action id="Kilo.WorktreeSession.Rename"
|
||||
class="ai.kilocode.client.actions.RenameWorktreeSessionAction"/>
|
||||
|
||||
@@ -250,6 +256,9 @@
|
||||
<group id="Kilo.Worktree.RowMenu">
|
||||
<reference ref="Kilo.Worktree.Rename"/>
|
||||
<separator/>
|
||||
<reference ref="Kilo.Worktree.OpenPr"/>
|
||||
<reference ref="Kilo.Worktree.OpenDiff"/>
|
||||
<separator/>
|
||||
<reference ref="Kilo.Worktree.Delete"/>
|
||||
</group>
|
||||
|
||||
|
||||
@@ -369,7 +369,8 @@ 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.pr.tooltip=Pull request #{0} ({1})
|
||||
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.gh.missing.title=GitHub CLI not found
|
||||
worktree.gh.missing.content=Install gh to show pull request badges for worktrees.
|
||||
worktree.gh.install=Install
|
||||
@@ -794,6 +795,10 @@ action.Kilo.Worktree.Rename.text=Rename
|
||||
action.Kilo.Worktree.Rename.description=Rename the selected worktree
|
||||
action.Kilo.Worktree.Delete.text=Delete
|
||||
action.Kilo.Worktree.Delete.description=Delete the selected worktree
|
||||
action.Kilo.Worktree.OpenPr.text=Open Pull Request
|
||||
action.Kilo.Worktree.OpenPr.description=Open the pull request in your browser
|
||||
action.Kilo.Worktree.OpenDiff.text=Open Diff
|
||||
action.Kilo.Worktree.OpenDiff.description=Open the diff against the base branch
|
||||
action.Kilo.WorktreeSession.Rename.text=Rename
|
||||
action.Kilo.WorktreeSession.Rename.description=Rename the selected worktree session
|
||||
action.Kilo.WorktreeSession.Delete.text=Delete
|
||||
|
||||
+11
@@ -341,6 +341,8 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
|
||||
assertTrue(xml.contains("id=\"Kilo.Session.Delete\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.Worktree.Rename\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.Worktree.Delete\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.Worktree.OpenPr\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.Worktree.OpenDiff\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.WorktreeSession.Rename\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.WorktreeSession.Delete\""))
|
||||
assertTrue(xml.contains("id=\"Kilo.Worktree.RowMenu\""))
|
||||
@@ -352,9 +354,18 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
|
||||
assertTrue(xml.contains("ref=\"Kilo.Session.Delete\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.Worktree.Rename\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.Worktree.Delete\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.Worktree.OpenPr\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.Worktree.OpenDiff\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.WorktreeSession.Rename\""))
|
||||
assertTrue(xml.contains("ref=\"Kilo.WorktreeSession.Delete\""))
|
||||
assertTrue(xml.contains("ref=\"${'$'}Copy\""))
|
||||
|
||||
// Row menu order: rename, (separator), open pr, open diff, (separator), delete.
|
||||
val rename = xml.indexOf("ref=\"Kilo.Worktree.Rename\"")
|
||||
val openPr = xml.indexOf("ref=\"Kilo.Worktree.OpenPr\"")
|
||||
val openDiff = xml.indexOf("ref=\"Kilo.Worktree.OpenDiff\"")
|
||||
val delete = xml.indexOf("ref=\"Kilo.Worktree.Delete\"")
|
||||
assertTrue(rename in 0 until openPr && openPr < openDiff && openDiff < delete)
|
||||
}
|
||||
|
||||
// ------ Helpers ------
|
||||
|
||||
+38
@@ -9,6 +9,7 @@ import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeStatusService
|
||||
import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.worktreeSessionParams
|
||||
import ai.kilocode.client.diff.KiloDiffEditorKind
|
||||
import ai.kilocode.client.testing.FakeWorktreeRpcApi
|
||||
import ai.kilocode.client.testing.TestCoroutines
|
||||
import ai.kilocode.client.testing.TestUiTimers
|
||||
@@ -22,7 +23,11 @@ import ai.kilocode.client.vfs.KiloPath
|
||||
import ai.kilocode.client.vfs.KiloVfsManager
|
||||
import ai.kilocode.client.vfs.KiloVirtualFile
|
||||
import ai.kilocode.client.vfs.KiloVirtualFileSystem
|
||||
import ai.kilocode.rpc.dto.GhAvailability
|
||||
import ai.kilocode.rpc.dto.GhState
|
||||
import ai.kilocode.rpc.dto.WorktreeDto
|
||||
import ai.kilocode.rpc.dto.WorktreePrDto
|
||||
import ai.kilocode.rpc.dto.WorktreePrListDto
|
||||
import ai.kilocode.rpc.dto.WorktreeStatsDto
|
||||
import ai.kilocode.rpc.dto.WorktreeStatsListDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
@@ -416,6 +421,39 @@ class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
assertEquals(3, metrics.behind)
|
||||
}
|
||||
|
||||
fun `test open diff opens the branch diff editor`() {
|
||||
val item = WorktreeDto("${project.basePath!!}/.kilo/worktrees/feature-x", "feature-x", "feature/x", "${project.basePath!!}/.kilo/worktrees/feature-x")
|
||||
rpc.listed += item
|
||||
val controller = WorktreeController(service, project.basePath!!, coroutines.scope)
|
||||
val panel = edt { AgentManagerPanel(testRootDisposable, controller, project) }
|
||||
edt { controller.reload() }
|
||||
flush()
|
||||
|
||||
assertTrue(edt { panel.canOpenDiff(item) })
|
||||
edt { panel.openDiff(item) }
|
||||
|
||||
val file = edt { FileEditorManager.getInstance(project).openFiles.single() as KiloVirtualFile }
|
||||
assertEquals(KiloDiffEditorKind.ID, file.path.kind)
|
||||
assertEquals(item.path, file.path.params["directory"])
|
||||
}
|
||||
|
||||
fun `test open pr availability reflects pr status`() {
|
||||
val item = WorktreeDto("${project.basePath!!}/.kilo/worktrees/feature-x", "feature-x", "feature/x", "${project.basePath!!}/.kilo/worktrees/feature-x")
|
||||
rpc.listed += item
|
||||
rpc.prResult = WorktreePrListDto(GhAvailability.OK, listOf(WorktreePrDto(item.path, 7, GhState.OPEN, "https://example.test/pr/7")))
|
||||
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()
|
||||
|
||||
assertTrue(edt { panel.canOpenPr(item) })
|
||||
assertFalse(edt { panel.canOpenPr(null) })
|
||||
}
|
||||
|
||||
fun `test worktree row hides badge while pending or deleting`() {
|
||||
val path = "feature/y"
|
||||
val activity = MutableStateFlow(mapOf(
|
||||
|
||||
+1
-1
@@ -919,7 +919,7 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
val area = activeListCellBounds(view.list, 0, selected = true).getValue(ACTIVE_LIST_CHANGES_CELL)
|
||||
assertEquals(KiloBundle.message("worktree.stats.diff.tooltip", 3, 2), view.list.getToolTipText(event(view.list, center(area))))
|
||||
assertEquals(KiloBundle.message("worktree.stats.tooltip", 0, 0, 3, 2), view.list.getToolTipText(event(view.list, center(area))))
|
||||
|
||||
hover(view, center(area))
|
||||
assertEquals(Cursor.HAND_CURSOR, view.list.cursor.type)
|
||||
|
||||
Reference in New Issue
Block a user