mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(jetbrains): show running badges in session history
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Show running badges on active sessions in JetBrains recent and history lists.
|
||||
+15
@@ -32,6 +32,11 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal data class SessionActivitySnapshot(
|
||||
val active: Set<String>,
|
||||
val changed: Set<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Project-level frontend service for session management.
|
||||
*
|
||||
@@ -85,6 +90,14 @@ class KiloSessionService internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun activity(previous: Set<String>): SessionActivitySnapshot {
|
||||
val active = statuses.value
|
||||
.filterValues { it.type == "busy" }
|
||||
.keys
|
||||
.toSet()
|
||||
return SessionActivitySnapshot(active, active xor previous)
|
||||
}
|
||||
|
||||
suspend fun list(dir: String): SessionListDto {
|
||||
val result = call { list(dir) }
|
||||
_sessions.value = result.sessions
|
||||
@@ -229,3 +242,5 @@ class KiloSessionService internal constructor(
|
||||
suspend fun pendingQuestions(dir: String): List<QuestionRequestDto> =
|
||||
call { pendingQuestions(dir) }
|
||||
}
|
||||
|
||||
private infix fun <T> Set<T>.xor(other: Set<T>): Set<T> = (this - other) + (other - this)
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ class SessionUi(
|
||||
}
|
||||
|
||||
is SessionControllerEvent.ViewChanged.ShowRecents -> {
|
||||
val panel = EmptySessionPanel(this, controller, event.recents) { manager?.showHistory() }
|
||||
val panel = EmptySessionPanel(this, controller, sessions, event.recents) { manager?.showHistory() }
|
||||
scroll.show(panel.view)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -60,6 +60,8 @@ class HistoryController(
|
||||
reloadCloud()
|
||||
}
|
||||
|
||||
internal fun activity(previous: Set<String>) = sessions.activity(previous)
|
||||
|
||||
fun reloadLocal() {
|
||||
edt { local.start() }
|
||||
cs.launch {
|
||||
|
||||
+36
-4
@@ -1,6 +1,8 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.GroupHeaderSeparator
|
||||
@@ -11,8 +13,10 @@ import com.intellij.util.ui.EmptyIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
@@ -24,6 +28,7 @@ private const val DELETE_AREA_WIDTH = 32
|
||||
internal open class HistoryRenderer<T : HistoryItem>(
|
||||
private val model: HistoryModel<T>,
|
||||
private val deletable: Boolean,
|
||||
private val active: () -> Set<String>,
|
||||
) : JPanel(BorderLayout()), ListCellRenderer<T> {
|
||||
companion object {
|
||||
private val icon: Icon = AllIcons.Actions.GC
|
||||
@@ -55,14 +60,19 @@ internal open class HistoryRenderer<T : HistoryItem>(
|
||||
add(sep, BorderLayout.NORTH)
|
||||
}
|
||||
private val title = SimpleColoredComponent()
|
||||
private val badge = BadgeLabel()
|
||||
private val time = JBLabel()
|
||||
private val del = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
}
|
||||
private val head = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
add(title)
|
||||
add(badge)
|
||||
}
|
||||
private val main = JPanel(BorderLayout()).apply {
|
||||
add(title, BorderLayout.CENTER)
|
||||
add(head, BorderLayout.CENTER)
|
||||
add(time, BorderLayout.EAST)
|
||||
}
|
||||
private val row = JPanel(BorderLayout()).apply {
|
||||
@@ -75,7 +85,7 @@ internal open class HistoryRenderer<T : HistoryItem>(
|
||||
isOpaque = true
|
||||
top.isOpaque = true
|
||||
row.border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg())
|
||||
UiStyle.Components.transparent(row, main, title, time, del)
|
||||
UiStyle.Components.transparent(row, main, head, title, badge, time, del)
|
||||
wrap.setContent(row)
|
||||
add(top, BorderLayout.NORTH)
|
||||
add(wrap, BorderLayout.CENTER)
|
||||
@@ -106,13 +116,35 @@ internal open class HistoryRenderer<T : HistoryItem>(
|
||||
)
|
||||
time.text = value?.let(HistoryTime::relative).orEmpty()
|
||||
time.foreground = weak
|
||||
badge.isVisible = value?.id in active()
|
||||
if (deletable) del.icon = if (selected) icon else empty
|
||||
|
||||
top.invalidate()
|
||||
return this
|
||||
}
|
||||
|
||||
internal fun runningVisible() = badge.isVisible
|
||||
|
||||
private class BadgeLabel : JBLabel(
|
||||
FilledBadgeIcon(
|
||||
KiloBundle.message("session.part.tool.running"),
|
||||
UiStyle.Colors.runningBadgeBg(),
|
||||
UiStyle.Colors.runningBadgeFg(),
|
||||
)
|
||||
) {
|
||||
init {
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
alignmentY = Component.CENTER_ALIGNMENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalHistoryRenderer(model: HistoryModel<LocalHistoryItem>) : HistoryRenderer<LocalHistoryItem>(model, deletable = true)
|
||||
internal class LocalHistoryRenderer(
|
||||
model: HistoryModel<LocalHistoryItem>,
|
||||
active: () -> Set<String> = { emptySet() },
|
||||
) : HistoryRenderer<LocalHistoryItem>(model, deletable = true, active)
|
||||
|
||||
internal class CloudHistoryRenderer(model: HistoryModel<CloudHistoryItem>) : HistoryRenderer<CloudHistoryItem>(model, deletable = false)
|
||||
internal class CloudHistoryRenderer(
|
||||
model: HistoryModel<CloudHistoryItem>,
|
||||
active: () -> Set<String> = { emptySet() },
|
||||
) : HistoryRenderer<CloudHistoryItem>(model, deletable = false, active)
|
||||
|
||||
+41
-5
@@ -16,6 +16,7 @@ import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.DataProvider
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.PopupHandler
|
||||
import com.intellij.ui.SearchTextField
|
||||
@@ -48,6 +49,7 @@ import javax.swing.JList
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.Timer
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
@@ -60,6 +62,7 @@ class HistoryPanel(
|
||||
) : BorderLayoutPanel(), Disposable, DataProvider {
|
||||
private val localSearch = search(controller.local)
|
||||
private val cloudSearch = search(controller.cloud)
|
||||
private var active = emptySet<String>()
|
||||
private val localList = localList()
|
||||
private val cloudList = cloudList()
|
||||
private val more = LoadMoreButton()
|
||||
@@ -80,6 +83,7 @@ class HistoryPanel(
|
||||
.setText(KiloBundle.message("history.tab.cloud"))
|
||||
.setForeSideComponent(back())
|
||||
private var stale = false
|
||||
private val timer = Timer(ACTIVITY_MS) { syncActivity() }
|
||||
private val tabs: JBTabs = JBTabsFactory.createTabs(null, this).apply {
|
||||
presentation.setSingleRow(true)
|
||||
presentation.setTabsPosition(JBTabsPosition.top)
|
||||
@@ -105,11 +109,14 @@ class HistoryPanel(
|
||||
}
|
||||
addHierarchyListener { e ->
|
||||
if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener
|
||||
if (isShowing && stale) {
|
||||
refresh()
|
||||
if (isShowing) {
|
||||
syncActivity()
|
||||
timer.start()
|
||||
if (stale) refresh()
|
||||
return@addHierarchyListener
|
||||
}
|
||||
if (!isShowing) stale = true
|
||||
timer.stop()
|
||||
stale = true
|
||||
}
|
||||
body.add(load, CARD_LOAD)
|
||||
body.add(tabs.component, CARD_TABS)
|
||||
@@ -215,7 +222,7 @@ class HistoryPanel(
|
||||
private fun localList() = JBList(controller.local).apply {
|
||||
selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
|
||||
isFocusable = true
|
||||
cellRenderer = LocalHistoryRenderer(controller.local)
|
||||
cellRenderer = LocalHistoryRenderer(controller.local) { active }
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
@@ -243,7 +250,7 @@ class HistoryPanel(
|
||||
private fun cloudList() = JBList(controller.cloud).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
isFocusable = true
|
||||
cellRenderer = CloudHistoryRenderer(controller.cloud)
|
||||
cellRenderer = CloudHistoryRenderer(controller.cloud) { active }
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
@@ -284,6 +291,22 @@ class HistoryPanel(
|
||||
repaint()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun syncActivity() {
|
||||
val snap = controller.activity(active)
|
||||
active = snap.active
|
||||
repaintRows(localList, controller.local, snap.changed)
|
||||
repaintRows(cloudList, controller.cloud, snap.changed)
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> repaintRows(list: JBList<T>, model: HistoryModel<T>, ids: Set<String>) {
|
||||
if (ids.isEmpty()) return
|
||||
model.visibleItems.forEachIndexed { index, item ->
|
||||
if (item.id !in ids) return@forEachIndexed
|
||||
list.getCellBounds(index, index)?.let(list::repaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loading(): Boolean {
|
||||
if (controller.local.loaded || controller.cloud.loaded) return false
|
||||
return controller.local.loading || controller.cloud.loading
|
||||
@@ -418,6 +441,17 @@ class HistoryPanel(
|
||||
return items.indices.mapNotNull { HistoryRenderer.section(items, it) }
|
||||
}
|
||||
|
||||
internal fun runningBadgeVisible(index: Int): Boolean {
|
||||
val list = activeList()
|
||||
val item = list.model.getElementAt(index) ?: return false
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val renderer = list.cellRenderer as javax.swing.ListCellRenderer<HistoryItem>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val typed = list as JList<HistoryItem>
|
||||
val view = renderer.getListCellRendererComponent(typed, item, index, false, false)
|
||||
return view is HistoryRenderer<*> && view.runningVisible()
|
||||
}
|
||||
|
||||
internal fun repoOnlyVisible() = repoOnly.isVisible
|
||||
|
||||
internal fun repoOnlySelected() = repoOnly.isSelected
|
||||
@@ -445,6 +479,7 @@ class HistoryPanel(
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
timer.stop()
|
||||
controller.onRepoOnlyChanged = null
|
||||
}
|
||||
|
||||
@@ -495,5 +530,6 @@ class HistoryPanel(
|
||||
private companion object {
|
||||
const val CARD_LOAD = "load"
|
||||
const val CARD_TABS = "tabs"
|
||||
const val ACTIVITY_MS = 10_000
|
||||
}
|
||||
}
|
||||
|
||||
+63
-7
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.history.HistoryTime
|
||||
@@ -10,6 +11,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Align
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
@@ -20,6 +22,7 @@ import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.Centerizer
|
||||
@@ -31,9 +34,11 @@ import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.HierarchyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionAdapter
|
||||
@@ -42,6 +47,7 @@ import javax.swing.JButton
|
||||
import javax.swing.JList
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.Timer
|
||||
|
||||
/**
|
||||
* Empty-session panel.
|
||||
@@ -52,6 +58,7 @@ import javax.swing.ListSelectionModel
|
||||
class EmptySessionPanel(
|
||||
parent: Disposable,
|
||||
private val controller: SessionController,
|
||||
private val sessions: KiloSessionService,
|
||||
recents: List<SessionDto>,
|
||||
private val history: () -> Unit = {},
|
||||
) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget {
|
||||
@@ -60,6 +67,8 @@ class EmptySessionPanel(
|
||||
private val model = DefaultListModel<LocalHistoryItem>()
|
||||
private var hover = -1
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var active = emptySet<String>()
|
||||
private val timer = Timer(ACTIVITY_MS) { syncActivity() }
|
||||
|
||||
private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
@@ -124,6 +133,15 @@ class EmptySessionPanel(
|
||||
isOpaque = false
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
setSessions(recents)
|
||||
addHierarchyListener { e ->
|
||||
if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener
|
||||
if (isShowing) {
|
||||
syncActivity()
|
||||
timer.start()
|
||||
return@addHierarchyListener
|
||||
}
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
val gap = UiStyle.Gap.pad()
|
||||
layout = BorderLayout(0, gap)
|
||||
@@ -220,6 +238,21 @@ class EmptySessionPanel(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun syncActivity() {
|
||||
val snap = sessions.activity(active)
|
||||
active = snap.active
|
||||
repaintRows(snap.changed)
|
||||
}
|
||||
|
||||
private fun repaintRows(ids: Set<String>) {
|
||||
if (ids.isEmpty()) return
|
||||
repeat(model.size()) { index ->
|
||||
if (model.getElementAt(index).id !in ids) return@repeat
|
||||
list.getCellBounds(index, index)?.let(list::repaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun index(e: MouseEvent): Int {
|
||||
val idx = list.locationToIndex(e.point)
|
||||
if (idx < 0) return -1
|
||||
@@ -230,12 +263,30 @@ class EmptySessionPanel(
|
||||
|
||||
private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer<LocalHistoryItem> {
|
||||
private val title = JBLabel()
|
||||
private val badge = JBLabel(
|
||||
FilledBadgeIcon(
|
||||
KiloBundle.message("session.part.tool.running"),
|
||||
UiStyle.Colors.runningBadgeBg(),
|
||||
UiStyle.Colors.runningBadgeFg(),
|
||||
)
|
||||
).apply {
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
}
|
||||
private val time = JBLabel()
|
||||
private val head = BorderLayoutPanel().apply {
|
||||
add(BorderLayoutPanel().apply {
|
||||
layout = FlowLayout(FlowLayout.LEFT, 0, 0)
|
||||
isOpaque = false
|
||||
add(title)
|
||||
add(badge)
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout(UiStyle.Gap.pad(), 0)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg())
|
||||
add(title, BorderLayout.CENTER)
|
||||
head.isOpaque = false
|
||||
add(head, BorderLayout.CENTER)
|
||||
add(time, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
@@ -246,13 +297,14 @@ class EmptySessionPanel(
|
||||
selected: Boolean,
|
||||
focus: Boolean,
|
||||
): Component {
|
||||
val active = selected || hover == index
|
||||
isOpaque = active
|
||||
background = if (active) list.selectionBackground else list.background
|
||||
title.foreground = if (active) list.selectionForeground else UIUtil.getLabelForeground()
|
||||
time.foreground = if (active) list.selectionForeground else UIUtil.getContextHelpForeground()
|
||||
val over = selected || hover == index
|
||||
isOpaque = over
|
||||
background = if (over) list.selectionBackground else list.background
|
||||
title.foreground = if (over) list.selectionForeground else UIUtil.getLabelForeground()
|
||||
time.foreground = if (over) list.selectionForeground else UIUtil.getContextHelpForeground()
|
||||
title.text = value?.let(::title) ?: ""
|
||||
time.text = value?.let(HistoryTime::relative) ?: ""
|
||||
badge.isVisible = value?.id in active
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -301,7 +353,7 @@ class EmptySessionPanel(
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
@@ -315,4 +367,8 @@ class EmptySessionPanel(
|
||||
private fun welcomeHtml() = XmlStringUtil.wrapInHtml(
|
||||
"<div style='text-align:center'>${XmlStringUtil.escapeString(KiloBundle.message("session.empty.welcome"))}</div>"
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val ACTIVITY_MS = 10_000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,16 @@ object UiStyle {
|
||||
/** Filled badge text color paired with [badgeBg]. */
|
||||
fun badgeFg(): Color = JBColor(Color.BLACK, UIUtil.getLabelForeground())
|
||||
|
||||
fun runningBadgeBg(): Color = JBColor.namedColor(
|
||||
"Kilo.History.runningBadgeBackground",
|
||||
JBColor(0xF5C542, 0x7A5A00),
|
||||
)
|
||||
|
||||
fun runningBadgeFg(): Color = JBColor.namedColor(
|
||||
"Kilo.History.runningBadgeForeground",
|
||||
JBColor(Color.BLACK, Color.WHITE),
|
||||
)
|
||||
|
||||
/** Card border color shared across profile cards. */
|
||||
fun cardBorder(): Color = JBColor.namedColor("Component.borderColor", JBColor.border())
|
||||
|
||||
|
||||
+2
-1
@@ -72,7 +72,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
val rpc = session("ses_1")
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager, workspaces = workspaces)
|
||||
val controller = controller(ui)
|
||||
val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, listOf(rpc))
|
||||
val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, sessions, listOf(rpc))
|
||||
|
||||
panel.clickRecent(0)
|
||||
|
||||
@@ -87,6 +87,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
val panel = ai.kilocode.client.session.ui.EmptySessionPanel(
|
||||
testRootDisposable,
|
||||
controller,
|
||||
sessions,
|
||||
emptyList(),
|
||||
) { manager.showHistory() }
|
||||
|
||||
|
||||
+51
@@ -12,6 +12,7 @@ import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -115,6 +116,56 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
assertTrue(controller.local.items.isEmpty())
|
||||
}
|
||||
|
||||
fun `test activity returns busy ids and changed ids`() {
|
||||
rpc.statuses.value = mapOf(
|
||||
"ses_busy" to SessionStatusDto("busy"),
|
||||
"ses_idle" to SessionStatusDto("idle"),
|
||||
"ses_retry" to SessionStatusDto("retry"),
|
||||
"ses_offline" to SessionStatusDto("offline"),
|
||||
)
|
||||
flush()
|
||||
|
||||
val snap = sessions.activity(setOf("ses_old", "ses_busy"))
|
||||
|
||||
assertEquals(setOf("ses_busy"), snap.active)
|
||||
assertEquals(setOf("ses_old"), snap.changed)
|
||||
}
|
||||
|
||||
fun `test local history renderer shows running badge for active id`() {
|
||||
val item = LocalHistoryItem(session("ses_1", "Running"))
|
||||
val controller = controller()
|
||||
controller.local.replace(listOf(item))
|
||||
val renderer = LocalHistoryRenderer(controller.local) { setOf("ses_1") }
|
||||
|
||||
renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
|
||||
assertTrue(renderer.runningVisible())
|
||||
}
|
||||
|
||||
fun `test cloud history renderer hides running badge for inactive id`() {
|
||||
val item = CloudHistoryItem(cloud("cloud_1", "Cloud"))
|
||||
val controller = controller()
|
||||
controller.cloud.replace(listOf(item), null)
|
||||
val renderer = CloudHistoryRenderer(controller.cloud) { emptySet() }
|
||||
|
||||
renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
|
||||
assertFalse(renderer.runningVisible())
|
||||
}
|
||||
|
||||
fun `test history panel sync updates running badges`() {
|
||||
rpc.listed += session("ses_1", "Local One")
|
||||
val panel = HistoryPanel(parent, controller())
|
||||
flush()
|
||||
assertFalse(panel.runningBadgeVisible(0))
|
||||
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
flush()
|
||||
panel.syncActivity()
|
||||
|
||||
assertTrue(panel.runningBadgeVisible(0))
|
||||
}
|
||||
|
||||
fun `test panel filters and switches source`() {
|
||||
rpc.listed += session("ses_1", "Alpha")
|
||||
rpc.listed += session("ses_2", "Beta")
|
||||
|
||||
+41
-2
@@ -9,6 +9,7 @@ import ai.kilocode.client.session.history.HistoryTime
|
||||
import ai.kilocode.client.session.history.LocalHistoryItem
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeSessionRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
@@ -17,6 +18,7 @@ import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.components.JBLabel
|
||||
@@ -25,6 +27,8 @@ import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Cursor
|
||||
|
||||
@@ -34,6 +38,8 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
private lateinit var app: KiloAppService
|
||||
private lateinit var workspace: Workspace
|
||||
private lateinit var controller: SessionController
|
||||
private lateinit var rpc: FakeSessionRpcApi
|
||||
private lateinit var sessions: KiloSessionService
|
||||
private val opened = mutableListOf<String>()
|
||||
|
||||
override fun setUp() {
|
||||
@@ -46,10 +52,12 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
it.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY)
|
||||
})
|
||||
workspace = workspaces.workspace("/test")
|
||||
rpc = FakeSessionRpcApi()
|
||||
sessions = KiloSessionService(project, scope, rpc)
|
||||
controller = SessionController(
|
||||
parent = testRootDisposable,
|
||||
ref = null,
|
||||
sessions = KiloSessionService(project, scope, FakeSessionRpcApi()),
|
||||
sessions = sessions,
|
||||
workspace = workspace,
|
||||
app = app,
|
||||
cs = scope,
|
||||
@@ -193,6 +201,28 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
assertEquals("Untitled", label?.text)
|
||||
}
|
||||
|
||||
fun `test renderer shows running badge for busy recent session`() {
|
||||
val panel = panel(listOf(session("ses_1")))
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
flush()
|
||||
panel.syncActivity()
|
||||
|
||||
val cell = panel.rendererComponent(session("ses_1")) as BorderLayoutPanel
|
||||
|
||||
assertTrue(runningVisible(cell))
|
||||
}
|
||||
|
||||
fun `test renderer hides running badge for idle recent session`() {
|
||||
val panel = panel(listOf(session("ses_1")))
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("idle"))
|
||||
flush()
|
||||
panel.syncActivity()
|
||||
|
||||
val cell = panel.rendererComponent(session("ses_1")) as BorderLayoutPanel
|
||||
|
||||
assertFalse(runningVisible(cell))
|
||||
}
|
||||
|
||||
fun `test timestamp normalization handles seconds and milliseconds`() {
|
||||
assertEquals(1_700_000_000_000L, HistoryTime.millis(LocalHistoryItem(session("ses_1", 1_700_000_000))))
|
||||
assertEquals(1_700_000_000_000L, HistoryTime.millis(LocalHistoryItem(session("ses_1", 1_700_000_000_000))))
|
||||
@@ -209,7 +239,16 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
private fun panel(recents: List<SessionDto> = emptyList(), history: () -> Unit = {}) =
|
||||
EmptySessionPanel(testRootDisposable, controller, recents, history)
|
||||
EmptySessionPanel(testRootDisposable, controller, sessions, recents, history)
|
||||
|
||||
private fun flush() = runBlocking {
|
||||
delay(100)
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
|
||||
private fun runningVisible(cell: BorderLayoutPanel): Boolean = UIUtil.uiTraverser(cell)
|
||||
.filter(JBLabel::class.java)
|
||||
.any { it.isVisible && it.icon is FilledBadgeIcon }
|
||||
|
||||
private fun session(id: String, updated: Long = 2_000L, title: String = "Title $id") = SessionDto(
|
||||
id = id,
|
||||
|
||||
Reference in New Issue
Block a user