diff --git a/.changeset/jetbrains-history-hover-rename.md b/.changeset/jetbrains-history-hover-rename.md new file mode 100644 index 00000000000..4b99fc28ef4 --- /dev/null +++ b/.changeset/jetbrains-history-hover-rename.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Reveal rename and delete buttons on hover in JetBrains session history, rename sessions through an inline popover instead of a modal dialog, and move the selection to the neighbouring session after deleting the selected one. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt index dd2a617b219..812ddf25550 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt @@ -1,28 +1,12 @@ package ai.kilocode.client.actions -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.history.HistoryDataKeys -import ai.kilocode.client.session.history.title import ai.kilocode.client.session.SessionManager import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages class RenameSessionAction : AnAction() { - /** Overridable in tests to avoid showing a real modal dialog. */ - internal var input: (project: Project?, current: String) -> String? = { project, current -> - Messages.showInputDialog( - project, - KiloBundle.message("history.rename.prompt"), - KiloBundle.message("history.rename.title"), - null, - current, - null, - ) - } - override fun getActionUpdateThread() = ActionUpdateThread.EDT override fun update(e: AnActionEvent) { @@ -35,26 +19,8 @@ class RenameSessionAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val selection = e.getData(HistoryDataKeys.SELECTION) ?: return - val controller = e.getData(HistoryDataKeys.CONTROLLER) ?: return + val rename = e.getData(HistoryDataKeys.RENAME) ?: return val item = selection.selectedLocal.singleOrNull() ?: return - - val current = title(item) - controller.requestRename() - val raw = input(e.project, current) - if (raw == null) { - controller.cancelRename("cancelled") - return - } - val newTitle = raw.trim() - - if (newTitle.isBlank()) { - controller.cancelRename("blank") - return - } - if (newTitle == current) { - controller.cancelRename("unchanged") - return - } - controller.rename(item, newTitle) + rename(item) } } 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 d64c4c36169..7bba4d1b504 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 @@ -14,6 +14,8 @@ import ai.kilocode.client.agentManager.worktree.worktreeSessionParams import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.list.ACTIVE_LIST_DELETE_CELL +import ai.kilocode.client.ui.list.ACTIVE_LIST_RENAME_CELL import ai.kilocode.client.ui.list.ActiveList import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListCell @@ -22,6 +24,8 @@ import ai.kilocode.client.ui.list.ActiveListDeleteOptions import ai.kilocode.client.ui.list.ActiveListItem import ai.kilocode.client.ui.list.ActiveListSelection import ai.kilocode.client.ui.list.ActiveListSurface +import ai.kilocode.client.ui.list.activeListDeleteCell +import ai.kilocode.client.ui.list.activeListRenameCell import ai.kilocode.client.ui.list.activeListToolWindowBackground import ai.kilocode.client.vfs.KiloVfsManager import ai.kilocode.rpc.dto.RemoveWorktreeResultDto @@ -73,8 +77,8 @@ class AgentManagerPanel( showSearch = false, onCell = { key, id -> val item = item(key) ?: return@ActiveList - if (id == RENAME_CELL && renameable(item)) beginRename(item, id) - if (id == DELETE_CELL && deletable(item)) showDeletePopup(item, id) + if (id == ACTIVE_LIST_RENAME_CELL && renameable(item)) beginRename(item, id) + if (id == ACTIVE_LIST_DELETE_CELL && deletable(item)) showDeletePopup(item, id) }, onOpen = { row, focus -> val item = (row as? WorktreeRow)?.dto ?: return@ActiveList @@ -392,25 +396,10 @@ class AgentManagerPanel( } override val cells: List get() = if (dto.main || pending) emptyList() else listOf( - ActiveListCell( - RENAME_CELL, - KiloBundle.message("worktree.rename.action"), - icon = AllIcons.Actions.Edit, - iconOnly = true, - ), - ActiveListCell( - DELETE_CELL, - KiloBundle.message("worktree.delete.action"), - icon = AllIcons.Actions.GC, - iconOnly = true, - ), + activeListRenameCell(KiloBundle.message("worktree.rename.action")), + activeListDeleteCell(KiloBundle.message("worktree.delete.action")), ) } - - private companion object { - const val RENAME_CELL = "rename" - const val DELETE_CELL = "delete" - } } internal fun worktreeDeletable(item: WorktreeDto?, pending: Boolean): Boolean = item != null && !item.main && !pending diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt index 942fc93a690..1bdb4bd307d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt @@ -8,6 +8,8 @@ import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.HistorySection import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem +import ai.kilocode.client.ui.list.ACTIVE_LIST_DELETE_CELL +import ai.kilocode.client.ui.list.ACTIVE_LIST_RENAME_CELL import ai.kilocode.client.ui.list.ActiveList import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListCell @@ -18,6 +20,8 @@ import ai.kilocode.client.ui.list.ActiveListItem import ai.kilocode.client.ui.list.ActiveListRowHeight import ai.kilocode.client.ui.list.ActiveListSelection import ai.kilocode.client.ui.list.ActiveListSurface +import ai.kilocode.client.ui.list.activeListDeleteCell +import ai.kilocode.client.ui.list.activeListRenameCell import ai.kilocode.client.ui.list.activeListToolWindowBackground import ai.kilocode.rpc.dto.SessionDto import com.intellij.icons.AllIcons @@ -72,8 +76,8 @@ class WorktreeSessionEditorPanel( showSearch = false, enter = { true }, onCell = { key, id -> - if (id == RENAME_CELL) beginRename(key, RENAME_CELL) - if (id == DELETE_CELL) confirmDelete(listOf(key), DELETE_CELL) + if (id == ACTIVE_LIST_RENAME_CELL) beginRename(key, ACTIVE_LIST_RENAME_CELL) + if (id == ACTIVE_LIST_DELETE_CELL) confirmDelete(listOf(key), ACTIVE_LIST_DELETE_CELL) }, onOpen = { row, focus -> open(row, focus) }, ) @@ -341,24 +345,9 @@ class WorktreeSessionEditorPanel( get() { if (selectedKeys().size != 1) return emptyList() return listOf( - ActiveListCell( - RENAME_CELL, - KiloBundle.message("worktree.session.rename.action"), - icon = AllIcons.Actions.Edit, - iconOnly = true, - ), - ActiveListCell( - DELETE_CELL, - KiloBundle.message("worktree.session.delete.action"), - icon = AllIcons.Actions.GC, - iconOnly = true, - ), + activeListRenameCell(KiloBundle.message("worktree.session.rename.action")), + activeListDeleteCell(KiloBundle.message("worktree.session.delete.action")), ) } } - - private companion object { - const val RENAME_CELL = "rename" - const val DELETE_CELL = "delete" - } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt index 3c560038abc..5a05eefe904 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt @@ -13,4 +13,11 @@ data class HistorySelection( object HistoryDataKeys { val SELECTION: DataKey = DataKey.create("ai.kilocode.client.session.history.HistorySelection") val CONTROLLER: DataKey = DataKey.create("ai.kilocode.client.session.history.HistoryController") + + /** + * Opens the inline rename popover anchored to a local session row. Provided by [HistoryPanel] so + * the context-menu and RenameElement-shortcut rename share the same balloon as the hover pencil + * instead of a modal dialog. + */ + val RENAME: DataKey<(LocalHistoryItem) -> Unit> = DataKey.create("ai.kilocode.client.session.history.HistoryRename") } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index a5834b1bef0..65f485c1f7e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -9,6 +9,8 @@ import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.HAlign import ai.kilocode.client.ui.layout.VAlign import ai.kilocode.client.ui.layout.align +import ai.kilocode.client.ui.list.ACTIVE_LIST_DELETE_CELL +import ai.kilocode.client.ui.list.ACTIVE_LIST_RENAME_CELL import ai.kilocode.client.ui.list.ActiveList import ai.kilocode.client.ui.list.ActiveListConfig import ai.kilocode.client.ui.list.ActiveListDeleteOptions @@ -221,9 +223,11 @@ class HistoryPanel( showSearch = false, openOnClick = false, onCell = { key, id -> - if (id != HISTORY_DELETE_CELL) return@ActiveList val item = localRows.firstOrNull { it.key == key }?.item ?: return@ActiveList - showDeletePopup(listOf(item), HISTORY_DELETE_CELL) + when (id) { + ACTIVE_LIST_RENAME_CELL -> beginRename(item, id) + ACTIVE_LIST_DELETE_CELL -> showDeletePopup(listOf(item), id) + } }, onOpen = { row, _ -> activate(row) }, ).apply { @@ -274,15 +278,27 @@ class HistoryPanel( restore(cloudList, cloudRows) } + /** + * Rebuilds [list] while keeping selection stable. Surviving selected rows stay selected; when a + * selected row disappears (e.g. it was just deleted) selection slides to the row that took its + * slot — the following row, or the last row when the removed one was last — so the highlight + * moves naturally instead of vanishing. Selection clears only when nothing was selected or the + * list is now empty. + */ private fun restore(list: ActiveList, rows: List) { val keys = list.selectedKeys() + val anchor = list.selectedIndex() list.update(rows, ActiveListSelection.PreserveNoScroll) val indices = keys.mapNotNull { key -> rows.indexOfFirst { it.key == key }.takeIf { it >= 0 } }.toIntArray() - if (indices.isEmpty()) { - list.clearSelection() + if (indices.isNotEmpty()) { + list.setSelectionIndices(indices) return } - list.setSelectionIndices(indices) + if (keys.isNotEmpty() && rows.isNotEmpty()) { + list.selectIndex(anchor.coerceIn(0, rows.size - 1)) + return + } + list.clearSelection() } @RequiresEdt @@ -320,6 +336,7 @@ class HistoryPanel( override fun getData(dataId: String): Any? { if (SessionManager.KEY.`is`(dataId)) return manager if (HistoryDataKeys.CONTROLLER.`is`(dataId)) return controller + if (HistoryDataKeys.RENAME.`is`(dataId)) return { item: LocalHistoryItem -> beginRename(item) } if (HistoryDataKeys.SELECTION.`is`(dataId)) { val source = selectedSource() val local = if (source == HistorySource.LOCAL) { @@ -361,6 +378,21 @@ class HistoryPanel( showDeletePopup(items) } + /** + * Opens the inline rename popover anchored to [item]'s row (or its pencil cell), matching the + * worktree list. Committing sends the new title through the controller; the popover itself gates + * out blank and unchanged names, so no modal dialog is involved. + */ + private fun beginRename(item: LocalHistoryItem, cell: String? = null) { + controller.requestRename() + localList.rename( + item.id, + cell, + current = { title(item) }, + commit = { _, name -> controller.rename(item, name) }, + ) + } + internal fun itemCount() = activeRows().size internal fun selectedSource() = if (tabs.selectedInfo === cloudInfo) HistorySource.CLOUD else HistorySource.LOCAL diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryRows.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryRows.kt index aabf45f70dc..3014e826adc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryRows.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryRows.kt @@ -1,13 +1,11 @@ package ai.kilocode.client.session.history -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListCell import ai.kilocode.client.ui.list.ActiveListItem -import com.intellij.icons.AllIcons - -internal const val HISTORY_DELETE_CELL = "delete" +import ai.kilocode.client.ui.list.activeListDeleteCell +import ai.kilocode.client.ui.list.activeListRenameCell internal data class LocalHistoryRow( val item: LocalHistoryItem, @@ -28,14 +26,7 @@ internal data class LocalHistoryRow( override val cells: List get() { if (deleting) return emptyList() - return listOf( - ActiveListCell( - HISTORY_DELETE_CELL, - KiloBundle.message("common.delete"), - icon = AllIcons.Actions.GC, - iconOnly = true, - ), - ) + return listOf(activeListRenameCell(), activeListDeleteCell()) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListActions.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListActions.kt new file mode 100644 index 00000000000..327ef8a0a4c --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListActions.kt @@ -0,0 +1,27 @@ +package ai.kilocode.client.ui.list + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.icons.AllIcons + +/** + * Standard hover action-cell ids and factories shared by the reveal-on-hover lists (the worktree + * list, the worktree-session editor list, and session history). Centralising them keeps the + * pencil/trash buttons — their ids, icons, and `iconOnly` treatment — identical across every list + * instead of each panel re-declaring `"rename"`/`"delete"` and rebuilding the same [ActiveListCell]. + */ +internal const val ACTIVE_LIST_RENAME_CELL = "rename" +internal const val ACTIVE_LIST_DELETE_CELL = "delete" + +internal fun activeListRenameCell(label: String = KiloBundle.message("common.rename")) = ActiveListCell( + ACTIVE_LIST_RENAME_CELL, + label, + icon = AllIcons.Actions.Edit, + iconOnly = true, +) + +internal fun activeListDeleteCell(label: String = KiloBundle.message("common.delete")) = ActiveListCell( + ACTIVE_LIST_DELETE_CELL, + label, + icon = AllIcons.Actions.GC, + iconOnly = true, +) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt index 4f75456cfd9..a66db51e57e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionManager import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.CloudHistoryItem @@ -298,72 +297,20 @@ class HistorySessionActionsTest : BasePlatformTestCase() { // ------ RenameSessionAction.actionPerformed ------ - fun `test rename action calls controller with trimmed changed title`() { - rpc.listed += sessionDto("ses_1", "Original") - controller.reloadLocal() - flush() - - val item = controller.local.items[0] - val action = RenameSessionAction().apply { input = { _, _ -> " Renamed " } } - val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + fun `test rename action opens rename popover for single local selection`() { + val item = localItem("ses_1") + val renamed = mutableListOf() + val action = RenameSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) { renamed += it.id } action.actionPerformed(event) - flush() - assertEquals(listOf(Triple("ses_1", "/test", "Renamed")), rpc.renames) + assertEquals(listOf("ses_1"), renamed) } - fun `test rename action passes displayed current title to input`() { - rpc.listed += sessionDto("ses_1", "Original") - controller.reloadLocal() - flush() - - val prompts = mutableListOf() - val item = controller.local.items[0] - val action = RenameSessionAction().apply { - input = { _, current -> - prompts.add(current) - null - } - } - val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) - - action.actionPerformed(event) - flush() - - assertEquals(listOf("Original"), prompts) - assertTrue(rpc.renames.isEmpty()) - } - - fun `test rename action passes untitled fallback to input`() { - rpc.listed += sessionDto("ses_1", "") - controller.reloadLocal() - flush() - - val prompts = mutableListOf() - val item = controller.local.items[0] - val action = RenameSessionAction().apply { - input = { _, current -> - prompts.add(current) - null - } - } - val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) - - action.actionPerformed(event) - flush() - - assertEquals(listOf(KiloBundle.message("history.untitled")), prompts) - assertTrue(rpc.renames.isEmpty()) - } - - fun `test rename action ignores blank input`() { - rpc.listed += sessionDto("ses_1", "Original") - controller.reloadLocal() - flush() - - val item = controller.local.items[0] - val action = RenameSessionAction().apply { input = { _, _ -> " " } } + fun `test rename action does nothing without a rename provider`() { + val item = localItem("ses_1") + val action = RenameSessionAction() val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) action.actionPerformed(event) @@ -372,34 +319,15 @@ class HistorySessionActionsTest : BasePlatformTestCase() { assertTrue(rpc.renames.isEmpty()) } - fun `test rename action ignores unchanged input`() { - rpc.listed += sessionDto("ses_1", "Original") - controller.reloadLocal() - flush() - - val item = controller.local.items[0] - val action = RenameSessionAction().apply { input = { _, _ -> "Original" } } - val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + fun `test rename action does nothing for multiple local selection`() { + val items = listOf(localItem("ses_1"), localItem("ses_2")) + val renamed = mutableListOf() + val action = RenameSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, items), controller) { renamed += it.id } action.actionPerformed(event) - flush() - assertTrue(rpc.renames.isEmpty()) - } - - fun `test rename action ignores null input`() { - rpc.listed += sessionDto("ses_1", "Original") - controller.reloadLocal() - flush() - - val item = controller.local.items[0] - val action = RenameSessionAction().apply { input = { _, _ -> null } } - val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) - - action.actionPerformed(event) - flush() - - assertTrue(rpc.renames.isEmpty()) + assertTrue(renamed.isEmpty()) } fun `test frontend descriptor registers history actions`() { @@ -426,6 +354,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() { manager: SessionManager?, selection: HistorySelection, ctrl: HistoryController, + rename: ((LocalHistoryItem) -> Unit)? = null, ): AnActionEvent { val presentation = Presentation().apply { copyFrom(action.templatePresentation) } val context = DataContext { id -> @@ -434,6 +363,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() { SessionManager.KEY.`is`(id) -> manager HistoryDataKeys.SELECTION.`is`(id) -> selection HistoryDataKeys.CONTROLLER.`is`(id) -> ctrl + HistoryDataKeys.RENAME.`is`(id) -> rename else -> null } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index d75bf864c5d..19cd05a9798 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -12,6 +12,8 @@ import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.client.testing.TestCoroutines import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.HoverIcon +import ai.kilocode.client.ui.list.ACTIVE_LIST_DELETE_CELL +import ai.kilocode.client.ui.list.ACTIVE_LIST_RENAME_CELL import ai.kilocode.client.ui.layout.Align import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto @@ -199,14 +201,13 @@ class HistoryControllerTest : BasePlatformTestCase() { assertTrue(row.badges.isEmpty()) } - fun `test local history rows use trailing time and delete cell`() { + fun `test local history rows use trailing time and rename and delete cells`() { val item = LocalHistoryItem(session("ses_1", "Long ".repeat(80))) val row = localHistoryRows(listOf(item), HistoryActivitySnapshot()) { false }[0] - val cell = row.cells.single() assertEquals(HistoryTime.relative(item), row.trailing) - assertEquals(HISTORY_DELETE_CELL, cell.id) - assertTrue(cell.iconOnly) + assertEquals(listOf(ACTIVE_LIST_RENAME_CELL, ACTIVE_LIST_DELETE_CELL), row.cells.map { it.id }) + assertTrue(row.cells.all { it.iconOnly }) } fun `test deleting local history rows hide actions and badges`() { @@ -631,6 +632,55 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(javax.swing.ListSelectionModel.SINGLE_SELECTION, panel.listSelectionMode()) } + // ------ Selection advances naturally on delete ------ + + fun `test deleting the selected session advances selection to the next row`() { + rpc.listed += session("ses_a", "A", 3.0) + rpc.listed += session("ses_b", "B", 2.0) + rpc.listed += session("ses_c", "C", 1.0) + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + panel.select(1) + + controller.delete(controller.local.items[1]) + flush() + + assertEquals(1, panel.selectedIndex()) + assertEquals("C", panel.titleText(1)) + } + + fun `test deleting the last selected session falls back to the previous row`() { + rpc.listed += session("ses_a", "A", 3.0) + rpc.listed += session("ses_b", "B", 2.0) + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + panel.select(1) + + controller.delete(controller.local.items[1]) + flush() + + assertEquals(0, panel.selectedIndex()) + assertEquals("A", panel.titleText(0)) + } + + fun `test deleting a background session keeps the current selection`() { + rpc.listed += session("ses_a", "A", 3.0) + rpc.listed += session("ses_b", "B", 2.0) + rpc.listed += session("ses_c", "C", 1.0) + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + panel.select(0) + + controller.delete(controller.local.items[2]) + flush() + + assertEquals(0, panel.selectedIndex()) + assertEquals("A", panel.titleText(0)) + } + // ------ Rename failure and directory selection ------ fun `test rename failure keeps original title and sets error`() {