mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): refine session list selection
This commit is contained in:
+14
-2
@@ -11,6 +11,8 @@ import ai.kilocode.client.session.SessionManager
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.SessionUi
|
||||
import ai.kilocode.client.session.SessionUiFactory
|
||||
import ai.kilocode.client.session.history.HistoryTime
|
||||
import ai.kilocode.client.session.history.LocalHistoryItem
|
||||
import ai.kilocode.client.util.UiTimerSource
|
||||
import ai.kilocode.client.util.UiTimers
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
@@ -118,6 +120,7 @@ open class WorktreeSessionEditorManager(
|
||||
val key = currentKey()
|
||||
val names = active.associateWith(::title)
|
||||
deleting.addAll(active)
|
||||
val target = if (key in active) next(key) else null
|
||||
onListChanged?.invoke()
|
||||
active.forEach { id ->
|
||||
val name = names[id] ?: title(id)
|
||||
@@ -130,8 +133,7 @@ open class WorktreeSessionEditorManager(
|
||||
}
|
||||
active.forEach(::forceSession)
|
||||
if (key in active) {
|
||||
val next = latest()
|
||||
if (next != null) openSession(SessionRef.Local(next)) else newSession()
|
||||
if (target != null) openSession(SessionRef.Local(target)) else newSession()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +160,16 @@ open class WorktreeSessionEditorManager(
|
||||
.maxByOrNull { it.time.updated }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun next(key: String?): SessionDto? {
|
||||
val rows = HistoryTime.sorted((0 until list.model.size).map { LocalHistoryItem(list.model.getElementAt(it)) })
|
||||
.map { it.session }
|
||||
val idx = rows.indexOfFirst { it.id == key }
|
||||
if (idx < 0) return rows.firstOrNull { it.id !in deleting }
|
||||
return rows.drop(idx + 1).firstOrNull { it.id !in deleting }
|
||||
?: rows.take(idx).asReversed().firstOrNull { it.id !in deleting }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun title(id: String): String {
|
||||
return (0 until list.model.size)
|
||||
|
||||
+10
-7
@@ -258,7 +258,7 @@ class WorktreeSessionEditorPanel(
|
||||
override val section: String get() = HistoryTime.title(HistorySection.TODAY)
|
||||
}
|
||||
|
||||
private data class SessionRow(
|
||||
private inner class SessionRow(
|
||||
val session: SessionDto,
|
||||
val kind: SessionActivityKind?,
|
||||
override val deleting: Boolean = false,
|
||||
@@ -272,12 +272,15 @@ class WorktreeSessionEditorPanel(
|
||||
override val section: String get() = HistoryTime.title(HistoryTime.section(item))
|
||||
override val search: String get() = listOf(session.title, session.id, session.directory).joinToString(" ")
|
||||
override val cells: List<ActiveListCell>
|
||||
get() = listOf(ActiveListCell(
|
||||
DELETE_CELL,
|
||||
KiloBundle.message("worktree.session.delete.action"),
|
||||
icon = AllIcons.Actions.GC,
|
||||
iconOnly = true,
|
||||
))
|
||||
get() {
|
||||
if (selectedKeys().size != 1) return emptyList()
|
||||
return listOf(ActiveListCell(
|
||||
DELETE_CELL,
|
||||
KiloBundle.message("worktree.session.delete.action"),
|
||||
icon = AllIcons.Actions.GC,
|
||||
iconOnly = true,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
+12
@@ -28,6 +28,7 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.event.ListSelectionEvent
|
||||
@@ -101,12 +102,14 @@ internal class ActiveListView(
|
||||
if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_PRESSED, true)) return
|
||||
list.requestFocusInWindow()
|
||||
press = null
|
||||
if (selection(e)) return
|
||||
val hit = hit(e) ?: return
|
||||
press = Press(hit.item.key, hit.id ?: return)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.clickCount == 1 && UIUtil.isActionClick(e, MouseEvent.MOUSE_CLICKED, true)) {
|
||||
if (selection(e)) return
|
||||
val hit = hit(e, enabled = false) ?: return
|
||||
if (hit.id != null) return
|
||||
if (hit.item.deleting) return
|
||||
@@ -126,6 +129,10 @@ internal class ActiveListView(
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return
|
||||
if (selection(e)) {
|
||||
press = null
|
||||
return
|
||||
}
|
||||
val down = press ?: return
|
||||
press = null
|
||||
val hit = hit(e) ?: return
|
||||
@@ -447,6 +454,11 @@ internal class ActiveListView(
|
||||
|
||||
private fun enter(): Boolean = AdvancedSettings.getBoolean(ENTER_FOCUS)
|
||||
|
||||
private fun selection(e: MouseEvent): Boolean {
|
||||
if (list.selectionMode == ListSelectionModel.SINGLE_SELECTION) return false
|
||||
return e.isShiftDown || e.isMetaDown || e.isControlDown
|
||||
}
|
||||
|
||||
override fun getScrollableTracksViewportWidth() = true
|
||||
|
||||
override fun getScrollableTracksViewportHeight() = false
|
||||
|
||||
+41
@@ -24,6 +24,8 @@ import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
import com.intellij.openapi.ui.TestDialog
|
||||
import com.intellij.openapi.ui.TestDialogManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.registry.RegistryKeyDescriptor
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -58,6 +60,7 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() {
|
||||
it.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY)
|
||||
})
|
||||
workspace = workspaces.workspace(DIR)
|
||||
useInactiveDisposeTimeout()
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
@@ -159,6 +162,25 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf(first.id to DIR), rpc.deletes.toList())
|
||||
}
|
||||
|
||||
fun `test deleting middle shown session falls back to next visible session`() {
|
||||
val top = session("ses_top", updated = 3.0)
|
||||
val mid = session("ses_mid", updated = 2.0)
|
||||
val bottom = session("ses_bottom", updated = 1.0)
|
||||
rpc.listed += top
|
||||
rpc.listed += mid
|
||||
rpc.listed += bottom
|
||||
val manager = manager()
|
||||
edt { manager.start() }
|
||||
flush()
|
||||
edt { manager.openSession(SessionRef.Local(mid), focus = false) }
|
||||
|
||||
edt { manager.deleteSessions(listOf(mid.id)) }
|
||||
pump()
|
||||
flush()
|
||||
|
||||
assertEquals(listOf(DIR to top.id, DIR to mid.id, DIR to bottom.id), created)
|
||||
}
|
||||
|
||||
fun `test delete marks session deleting then removes on success`() {
|
||||
val gate = CompletableDeferred<Unit>()
|
||||
rpc.deleteGate = gate
|
||||
@@ -251,6 +273,24 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() {
|
||||
assertTrue(edt(block))
|
||||
}
|
||||
|
||||
private fun useInactiveDisposeTimeout() {
|
||||
Registry.mutateContributedKeys {
|
||||
it + (TIMEOUT to RegistryKeyDescriptor(
|
||||
TIMEOUT,
|
||||
"Milliseconds before hidden session UI is disposed after switching away.",
|
||||
"180000",
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
))
|
||||
}
|
||||
Disposer.register(testRootDisposable) {
|
||||
Registry.mutateContributedKeys { it - TIMEOUT }
|
||||
}
|
||||
Registry.get(TIMEOUT).setValue(60_000, testRootDisposable)
|
||||
}
|
||||
|
||||
private fun pump() {
|
||||
com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
@@ -266,6 +306,7 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() {
|
||||
|
||||
private companion object {
|
||||
const val DIR = "/repo/.kilo/worktrees/feature-x"
|
||||
const val TIMEOUT = "kilo.session.inactive.disposeTimeoutMs"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -179,6 +179,30 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf(false), manager.focuses)
|
||||
}
|
||||
|
||||
fun `test modified row click preserves multi selection`() {
|
||||
rpc.listed += session("ses_1", 1.0)
|
||||
rpc.listed += session("ses_2", 2.0)
|
||||
edt { controller.reload() }
|
||||
flush()
|
||||
|
||||
val list = edt { UIUtil.findComponentOfType(panel, JBList::class.java)!! }
|
||||
edt {
|
||||
list.setSize(400, 100)
|
||||
list.doLayout()
|
||||
list.selectedIndices = intArrayOf(0, 1)
|
||||
val bounds = list.getCellBounds(1, 1)
|
||||
val x = bounds.x + 8
|
||||
val y = bounds.y + bounds.height / 2
|
||||
fire(list, MouseEvent(list, MouseEvent.MOUSE_CLICKED, System.currentTimeMillis(), InputEvent.SHIFT_DOWN_MASK, x, y, 1, false, MouseEvent.BUTTON1))
|
||||
assertEquals(listOf(0, 1), list.selectedIndices.toList())
|
||||
fire(list, MouseEvent(list, MouseEvent.MOUSE_CLICKED, System.currentTimeMillis(), InputEvent.META_DOWN_MASK, x, y, 1, false, MouseEvent.BUTTON1))
|
||||
assertEquals(listOf(0, 1), list.selectedIndices.toList())
|
||||
}
|
||||
|
||||
assertTrue(manager.refs.isEmpty())
|
||||
assertTrue(manager.focuses.isEmpty())
|
||||
}
|
||||
|
||||
fun `test row click ignores deleting session`() {
|
||||
manager.deletingIds += "ses_1"
|
||||
rpc.listed += session("ses_1", 1.0)
|
||||
@@ -247,6 +271,21 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("ses_2", "ses_1"), manager.deleted)
|
||||
}
|
||||
|
||||
fun `test multi select hides row delete cells`() {
|
||||
rpc.listed += session("ses_1", 1.0)
|
||||
rpc.listed += session("ses_2", 2.0)
|
||||
edt { controller.reload() }
|
||||
flush()
|
||||
|
||||
edt { panel.selectSessions(listOf("ses_1")) }
|
||||
assertEquals(listOf(DELETE_CELL), row("ses_1").cells.map { it.id })
|
||||
|
||||
edt { panel.selectSessions(listOf("ses_1", "ses_2")) }
|
||||
|
||||
assertTrue(row("ses_1").cells.isEmpty())
|
||||
assertTrue(row("ses_2").cells.isEmpty())
|
||||
}
|
||||
|
||||
fun `test delete action skips deleting selected sessions`() {
|
||||
manager.deletingIds += "ses_1"
|
||||
rpc.listed += session("ses_1", 1.0)
|
||||
@@ -386,5 +425,6 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() {
|
||||
|
||||
private companion object {
|
||||
const val DIR = "/repo/.kilo/worktrees/feature-x"
|
||||
const val DELETE_CELL = "delete"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user