From 319d1f485917266cc4a56d620c903afa2e96125d Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 3 Aug 2026 17:31:29 -0400 Subject: [PATCH] fix(jetbrains): stabilize worktree list updates --- .../jetbrains-worktree-list-stability.md | 5 + .../backend/app/KiloBackendActivityManager.kt | 2 +- .../app/KiloBackendActivityManagerTest.kt | 17 ++++ .../worktree/WorktreeController.kt | 10 +- .../worktree/WorktreeNameCache.kt | 4 + .../worktree/WorktreeSessionEditorManager.kt | 6 +- .../worktree/WorktreeSessionListController.kt | 11 +-- .../client/session/controller/DelayedState.kt | 11 +-- .../session/controller/SessionController.kt | 5 +- .../session/controller/SessionUpdateQueue.kt | 23 ++--- .../session/history/HistoryController.kt | 11 +-- .../kilocode/client/ui/list/ActiveListView.kt | 95 +++++++++++++------ .../kotlin/ai/kilocode/client/util/Edt.kt | 37 ++++++++ .../agentManager/AgentManagerPanelTest.kt | 8 +- .../worktree/WorktreeNameCacheTest.kt | 62 ++++++++++++ .../WorktreeSessionEditorManagerTest.kt | 8 +- .../WorktreeSessionEditorPanelTest.kt | 8 +- .../session/controller/DelayedStateTest.kt | 5 +- .../controller/SessionControllerTestBase.kt | 12 +-- .../client/session/model/SessionModelTest.kt | 8 +- .../session/views/LoginRequiredViewTest.kt | 8 +- .../views/base/BaseQuestionViewTest.kt | 8 +- .../settings/KiloSettingsConfigurableTest.kt | 8 +- .../settings/UserProfileConfigurableTest.kt | 8 +- .../agents/AgentBehaviorConfigurableTest.kt | 8 +- .../settings/agents/AgentCreateDialogTest.kt | 8 +- .../settings/agents/AgentEditDialogTest.kt | 8 +- .../settings/agents/AgentsSettingsUiTest.kt | 8 +- .../settings/agents/McpEditDialogTest.kt | 8 +- .../settings/agents/McpSettingsUiTest.kt | 8 +- .../settings/agents/SkillsSettingsUiTest.kt | 8 +- .../autoapprove/AutoApproveSettingsUiTest.kt | 8 +- .../autoapprove/SettingsInlineListTest.kt | 8 +- .../settings/base/BaseSettingsUiTest.kt | 8 +- .../base/BaseSettingsUiWorkspaceTest.kt | 8 +- .../base/KiloReadyConfigurableTest.kt | 8 +- .../settings/base/SettingsListViewTest.kt | 8 +- .../settings/context/ContextSettingsUiTest.kt | 8 +- .../settings/models/ModelsSettingsUiTest.kt | 8 +- .../providers/ProvidersSettingsUiTest.kt | 8 +- .../settings/rules/RulesSettingsUiTest.kt | 8 +- .../kilocode/client/testing/TestUiTimers.kt | 10 +- .../client/ui/list/ActiveListMatchTest.kt | 34 +++++++ .../client/ui/picker/PickerPopupTest.kt | 8 +- 44 files changed, 297 insertions(+), 271 deletions(-) create mode 100644 .changeset/jetbrains-worktree-list-stability.md create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/Edt.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListMatchTest.kt diff --git a/.changeset/jetbrains-worktree-list-stability.md b/.changeset/jetbrains-worktree-list-stability.md new file mode 100644 index 00000000000..0017f346a57 --- /dev/null +++ b/.changeset/jetbrains-worktree-list-stability.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve Agent Manager worktree activity refreshes and keep worktree list filtering responsive. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendActivityManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendActivityManager.kt index d54c793bdf8..c49cc73e3c9 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendActivityManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendActivityManager.kt @@ -45,9 +45,9 @@ class KiloBackendActivityManager( directory: (String) -> String?, chatEvents: SharedFlow, ) { + if (status?.isActive == true || events?.isActive == true) stop() this.statuses = statuses this.directory = directory - if (status?.isActive == true || events?.isActive == true) return status = cs.launch { statuses.collect { synchronized(lock) { recompute() } } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendActivityManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendActivityManagerTest.kt index 4a4c1a89314..32616590907 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendActivityManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendActivityManagerTest.kt @@ -103,6 +103,23 @@ class KiloBackendActivityManagerTest { assertFalse("ses_unknown" in snap) } + @Test + fun `start rebinds collectors to the latest flows`() = runBlocking { + val nextStatuses = MutableStateFlow>(emptyMap()) + val nextEvents = MutableSharedFlow(extraBufferCapacity = 16) + directories["ses_old"] = "/repo/old" + directories["ses_new"] = "/repo/new" + start() + + manager.start(nextStatuses, { directories[it] }, nextEvents) + statuses.value = mapOf("ses_old" to SessionStatusDto("busy")) + nextStatuses.value = mapOf("ses_new" to SessionStatusDto("busy")) + + val snap = await("ses_new", SessionActivityKindDto.RUNNING) + assertFalse("ses_old" in snap) + assertEquals("/repo/new", snap["ses_new"]?.directory) + } + private suspend fun await(id: String, kind: SessionActivityKindDto) = withTimeout(5_000) { manager.activity.first { it[id]?.kind == kind } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt index 31e7b653393..1888cbeb4f2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.edt import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.rpc.dto.CreateWorktreeRequestDto import ai.kilocode.rpc.dto.RemoveWorktreeResultDto @@ -226,12 +227,3 @@ class WorktreeController( return ApplicationManager.getApplication().service() } } - -private fun edt(block: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { - block() - return - } - app.invokeLater(block) -} 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 c5b1fcc8ed7..2fe40ddd38a 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 @@ -16,6 +16,7 @@ class WorktreeNameCache { private val names = linkedMapOf() private val listeners = mutableListOf<(String, String?) -> Unit>() + @RequiresEdt fun get(path: String): String? = names[path] @RequiresEdt @@ -33,6 +34,7 @@ class WorktreeNameCache { if (names.remove(path) != null) fire(path, null) } + @RequiresEdt fun clear() { names.clear() } @@ -41,10 +43,12 @@ class WorktreeNameCache { * Bulk sync from a worktree list reload. Does not notify: it mirrors the list model that * triggered it, so notifying would only echo back into that same list. */ + @RequiresEdt fun putAll(items: List) { items.forEach { names[it.path] = it.name } } + @RequiresEdt fun addListener(parent: Disposable, listener: (path: String, name: String?) -> Unit) { listeners.add(listener) Disposer.register(parent) { listeners.remove(listener) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt index 2ad4aae5561..b565237afbe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt @@ -15,6 +15,7 @@ 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.client.util.edt import ai.kilocode.client.vfs.KiloVfsManager import ai.kilocode.rpc.dto.RenameWorktreeResultDto import ai.kilocode.rpc.dto.SessionDto @@ -253,11 +254,6 @@ open class WorktreeSessionEditorManager( } } -private fun edt(block: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) block() else app.invokeLater(block) -} - // Mirrors the CLI's Session.isDefaultTitle (packages/opencode/src/session/session.ts): a session // keeps a "New session - " / "Child session - " placeholder until the agent names it. private val DEFAULT_SESSION_TITLE = diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionListController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionListController.kt index ce80da59876..04541db0182 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionListController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionListController.kt @@ -2,9 +2,9 @@ package ai.kilocode.client.agentManager.worktree import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.edt import ai.kilocode.log.KiloLog import ai.kilocode.rpc.dto.SessionDto -import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.CollectionListModel import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.CoroutineScope @@ -125,12 +125,3 @@ class WorktreeSessionListController( return (0 until model.size).firstOrNull { model.getElementAt(it).id == id } ?: -1 } } - -private fun edt(block: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { - block() - return - } - app.invokeLater(block) -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt index 960bf3b57a9..8b7859824dd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt @@ -2,8 +2,8 @@ package ai.kilocode.client.session.controller import ai.kilocode.client.util.UiTimerSource import ai.kilocode.client.util.UiTimers +import ai.kilocode.client.util.edt import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager internal class DelayedState( private val ms: Long, @@ -62,15 +62,6 @@ internal class DelayedState( return now + ms.coerceAtMost(Long.MAX_VALUE - now) } - private fun edt(block: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { - block() - return - } - app.invokeLater(block) - } - override fun dispose() { alive = false cancel() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 1370c72aa16..6373645a785 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -28,6 +28,7 @@ import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.client.util.UiTimer import ai.kilocode.client.util.UiTimerSource import ai.kilocode.client.util.UiTimers +import ai.kilocode.client.util.edtLater as edt import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.ConfigUpdateDto @@ -2322,10 +2323,6 @@ class SessionController( check(ApplicationManager.getApplication().isDispatchThread) { "SessionController state must be accessed on EDT" } } - private fun edt(block: () -> Unit) { - ApplicationManager.getApplication().invokeLater(block) - } - private fun runEdt(block: () -> Unit) { val application = ApplicationManager.getApplication() if (application.isDispatchThread) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt index ad27ed5e218..c24653df8c4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt @@ -1,10 +1,10 @@ package ai.kilocode.client.session.controller +import ai.kilocode.client.util.edt import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import ai.kilocode.rpc.dto.ChatEventDto import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer import java.awt.Component import java.awt.event.HierarchyEvent @@ -33,7 +33,6 @@ internal class SessionUpdateQueue( private val LOG = KiloLog.create(SessionUpdateQueue::class.java) } - private val app = ApplicationManager.getApplication() private val condenser = SessionQueueCondenser() private val pending = mutableListOf() private val lock = Any() @@ -58,7 +57,7 @@ internal class SessionUpdateQueue( init { Disposer.register(parent, this) - if (comp != null && watch != null) edt { + if (comp != null && watch != null) run { visible.set(comp.isShowing) comp.addHierarchyListener(watch) } @@ -81,7 +80,7 @@ internal class SessionUpdateQueue( fun holdFlush(hold: Boolean) { if (disposed.get()) return - edt { + run { LOG.debug { "${ChatLogSummary.sid(sid())} hold=$hold" } this.hold = hold } @@ -90,7 +89,7 @@ internal class SessionUpdateQueue( fun requestFlush(forced: Boolean, source: String = "api") { if (disposed.get()) return if (!forced && !visible.get()) return - edt { flushNow(forced, source) } + run { flushNow(forced, source) } } override fun dispose() { @@ -102,7 +101,7 @@ internal class SessionUpdateQueue( if (comp != null && watch != null) comp.removeHierarchyListener(watch) synchronized(lock) { pending.clear() } } - if (app.isDispatchThread) cleanup() else app.invokeLater(cleanup) + edt(cleanup) } private fun flushNow(forced: Boolean, source: String) { @@ -133,15 +132,5 @@ internal class SessionUpdateQueue( requestFlush(true, "visible") } - private fun edt(block: () -> Unit) { - if (disposed.get()) return - if (app.isDispatchThread) { - block() - return - } - app.invokeLater { - if (disposed.get()) return@invokeLater - block() - } - } + private fun run(block: () -> Unit) = edt({ !disposed.get() }, block) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index d43fb543699..fd49a6ff1bd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -5,9 +5,9 @@ import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionRef import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.edt import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto -import com.intellij.openapi.application.ApplicationManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -222,15 +222,6 @@ class HistoryController( } } -private fun edt(block: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { - block() - return - } - app.invokeLater(block) -} - private fun localItem(session: SessionDto) = LocalHistoryItem(session) private fun cloudItem(session: CloudSessionDto) = CloudHistoryItem(session) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt index b0471c556fc..aae03cf2d80 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt @@ -87,6 +87,7 @@ internal class ActiveListView( private var press: Press? = null private var popups = 0 private var hovered = -1 + private var heightKey: ActiveListHeightKey? = null internal var onSelect: (() -> Unit)? = null fun setEmptyText(text: String) { @@ -271,6 +272,7 @@ internal class ActiveListView( @RequiresEdt fun update(items: List, selection: ActiveListSelection = ActiveListSelection.Preserve) { checkEdt() + if (this.items != items) heightKey = null this.items = items val key = when (selection) { is ActiveListSelection.Key -> selection.key @@ -301,40 +303,13 @@ internal class ActiveListView( @RequiresEdt fun trackPopup(popup: JBPopup) { checkEdt() - var tracked = false - fun activate() { - if (tracked) return - tracked = true - popups++ - list.repaint() - } - popup.addListener(object : JBPopupListener { - override fun beforeShown(event: LightweightWindowEvent) = activate() - - override fun onClosed(event: LightweightWindowEvent) { - if (!tracked) return - tracked = false - popups = maxOf(0, popups - 1) - list.repaint() - } - }) - if (popup.isVisible) activate() + trackPopupState(popup.isVisible) { listener -> popup.addListener(listener) } } @RequiresEdt fun trackBalloon(balloon: Balloon) { checkEdt() - var tracked = true - popups++ - list.repaint() - balloon.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - if (!tracked) return - tracked = false - popups = maxOf(0, popups - 1) - list.repaint() - } - }) + trackPopupState(true) { listener -> balloon.addListener(listener) } } @RequiresEdt @@ -380,6 +355,9 @@ internal class ActiveListView( @RequiresEdt private fun syncCellHeight(rows: List) { checkEdt() + val key = ActiveListHeightKey(cfg, list.width, rows.map(::activeListHeightRow)) + if (heightKey == key) return + heightKey = key renderer.setBodyHeight(null) if (cfg.height == ActiveListRowHeight.PREFERRED) { if (list.fixedCellHeight == -1) return @@ -503,6 +481,27 @@ internal class ActiveListView( return Hit(item, id) } + private fun trackPopupState(visible: Boolean, add: (JBPopupListener) -> Unit) { + var tracked = false + fun activate() { + if (tracked) return + tracked = true + popups++ + list.repaint() + } + add(object : JBPopupListener { + override fun beforeShown(event: LightweightWindowEvent) = activate() + + override fun onClosed(event: LightweightWindowEvent) { + if (!tracked) return + tracked = false + popups = maxOf(0, popups - 1) + list.repaint() + } + }) + if (visible) activate() + } + private fun checkEdt() { check(ApplicationManager.getApplication().isDispatchThread) { "Active list updates must run on EDT" } } @@ -546,6 +545,42 @@ internal class ActiveListView( private data class Press(val key: String, val id: String) + private data class ActiveListHeightKey( + val cfg: ActiveListConfig, + val width: Int, + val rows: List, + ) + +} + +private data class ActiveListHeightRow( + val key: String, + val title: String, + val note: String?, + val description: String?, + val icon: Any?, + val section: String?, + val badges: List, + val trailing: String?, + val cells: List, + val disabled: Boolean, + val deleting: Boolean, +) + +private fun activeListHeightRow(item: ActiveListItem): ActiveListHeightRow { + return ActiveListHeightRow( + item.key, + item.title, + item.note, + item.description, + item.icon, + item.section, + item.badges, + item.trailing, + item.cells, + item.disabled, + item.deleting, + ) } private fun activeListIndex(items: List, key: String?): Int { @@ -582,9 +617,11 @@ private fun activeListTextMatches(query: String, text: String): Boolean { private fun activeListAcronym(text: String, query: String): Boolean { val words = activeListWords(text) + val seen = HashSet>() fun attempt(wi: Int, qi: Int): Boolean { if (qi == query.length) return true if (wi >= words.size) return false + if (!seen.add(wi to qi)) return false val word = words[wi] var count = 0 while (qi + count < query.length && count < word.length && word[count] == query[qi + count]) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/Edt.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/Edt.kt new file mode 100644 index 00000000000..37bf171c72a --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/Edt.kt @@ -0,0 +1,37 @@ +package ai.kilocode.client.util + +import com.intellij.openapi.application.ApplicationManager + +internal fun edt(block: () -> Unit) { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + block() + return + } + app.invokeLater(block) +} + +internal fun edt(alive: () -> Boolean, block: () -> Unit) { + if (!alive()) return + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + if (alive()) block() + return + } + app.invokeLater { + if (alive()) block() + } +} + +internal fun edtLater(block: () -> Unit) { + ApplicationManager.getApplication().invokeLater(block) +} + +internal fun edtWait(block: () -> T): T { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) return block() + var out: T? = null + app.invokeAndWait { out = block() } + @Suppress("UNCHECKED_CAST") + return out as T +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt index ffca461d95d..95f752e75a4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.agentManager +import ai.kilocode.client.util.edtWait import ai.kilocode.client.agentManager.worktree.KiloWorktreeService import ai.kilocode.client.agentManager.worktree.WorktreeController import ai.kilocode.client.agentManager.worktree.WorktreeEditorMatcher @@ -408,12 +409,7 @@ class AgentManagerPanelTest : BasePlatformTestCase() { flush() } - private fun edt(block: () -> T): T { - val out = arrayOfNulls(1) - ApplicationManager.getApplication().invokeAndWait { out[0] = block() } - @Suppress("UNCHECKED_CAST") - return out[0] as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flush() = coroutines.drain(::pump) 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 new file mode 100644 index 00000000000..a6f709d8a62 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeNameCacheTest.kt @@ -0,0 +1,62 @@ +package ai.kilocode.client.agentManager.worktree + +import ai.kilocode.client.util.edtWait +import ai.kilocode.rpc.dto.WorktreeDto +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +@Suppress("UnstableApiUsage") +class WorktreeNameCacheTest : BasePlatformTestCase() { + override fun tearDown() { + try { + edt { cache().clear() } + } finally { + super.tearDown() + } + } + + fun `test put and remove notify only when value changes`() = edt { + val events = mutableListOf>() + cache().addListener(testRootDisposable) { path, name -> events += path to name } + + cache().put("/repo/wt", "First") + cache().put("/repo/wt", "First") + cache().put("/repo/wt", "Second") + cache().remove("/repo/wt") + cache().remove("/repo/wt") + + assertEquals( + listOf("/repo/wt" to "First", "/repo/wt" to "Second", "/repo/wt" to null), + events, + ) + } + + fun `test bulk sync updates names without notifying`() = edt { + val events = mutableListOf>() + cache().addListener(testRootDisposable) { path, name -> events += path to name } + + cache().putAll(listOf(WorktreeDto("/repo/wt", "Feature", "feature/x", "/repo/wt"))) + + assertEquals("Feature", cache().get("/repo/wt")) + assertTrue(events.isEmpty()) + } + + fun `test listener is removed with parent disposable`() = edt { + val parent = Disposer.newDisposable("worktree-cache-listener") + Disposer.register(testRootDisposable, parent) + val events = mutableListOf>() + cache().addListener(parent) { path, name -> events += path to name } + + cache().put("/repo/one", "One") + Disposer.dispose(parent) + cache().put("/repo/two", "Two") + + assertEquals(listOf("/repo/one" to "One"), events) + } + + private fun cache(): WorktreeNameCache = ApplicationManager.getApplication().service() + + private fun edt(block: () -> T): T = edtWait(block) +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt index 308d0bf4b93..62360e29090 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.agentManager.worktree +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService @@ -373,12 +374,7 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() { } } - private fun edt(block: () -> T): T { - val out = arrayOfNulls(1) - com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait { out[0] = block() } - @Suppress("UNCHECKED_CAST") - return out[0] as T - } + private fun edt(block: () -> T): T = edtWait(block) private companion object { const val DIR = "/repo/.kilo/worktrees/feature-x" diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt index 5cab6d9b298..9cd51ba3e9d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.agentManager.worktree +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionActivityKind @@ -405,12 +406,7 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() { } } - private fun edt(block: () -> T): T { - val out = arrayOfNulls(1) - com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait { out[0] = block() } - @Suppress("UNCHECKED_CAST") - return out[0] as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun components(root: java.awt.Component): List { val out = mutableListOf() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt index 7b060ef0daf..17efedd55ad 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.controller +import ai.kilocode.client.util.edtWait import ai.kilocode.client.testing.TestUiTimers import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer @@ -138,7 +139,5 @@ class DelayedStateTest : BasePlatformTestCase() { return state } - private fun edt(block: () -> Unit) { - ApplicationManager.getApplication().invokeAndWait(block) - } + private fun edt(block: () -> Unit) = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt index 00c17bde29e..a6d179cbe24 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.controller +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.session.model.SessionModel @@ -267,16 +268,9 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { } } - protected fun edt(block: () -> Unit) { - ApplicationManager.getApplication().invokeAndWait(block) - } + protected fun edt(block: () -> Unit) = edtWait(block) - protected fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + protected fun edt(block: () -> T): T = edtWait(block) /** Emit a chat event into the fake RPC flow. */ protected fun emit(event: ChatEventDto, flush: Boolean = true) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt index a210429af0d..0c7fce27165 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.model +import ai.kilocode.client.util.edtWait import ai.kilocode.rpc.dto.DiffFileDto import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto @@ -1197,10 +1198,5 @@ class SessionModelTest : BasePlatformTestCase() { assertEquals(expected.trimIndent().trim(), model.toString().trim()) } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt index 5f651adcb81..fcdf2d85b18 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.views +import ai.kilocode.client.util.edtWait import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle @@ -185,12 +186,7 @@ class LoginRequiredViewTest : BasePlatformTestCase() { // ------ helpers ------ - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private inline fun findAll(root: Container): List = findAllCls(root, T::class.java) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt index 6018178065f..ffc0a1b97cf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.views.base +import ai.kilocode.client.util.edtWait import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle @@ -436,12 +437,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() { // ------ helpers ------ - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun region(panel: BaseQuestionView, region: String) = (panel.layout as BorderLayout).getLayoutComponent(region) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt index 5b7a77d0a74..c381b09ea71 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings +import ai.kilocode.client.util.edtWait import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.settings.context.ContextConfigurable import ai.kilocode.client.settings.models.ModelsConfigurable @@ -141,12 +142,7 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() { // -- helpers -- - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun links(root: Container): List = buildList { for (comp in root.components) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt index 42153250d21..eef2b246cdb 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.settings.profile.ProfileUi import ai.kilocode.client.settings.profile.formatResetDate @@ -1067,12 +1068,7 @@ class UserProfileConfigurableTest : BasePlatformTestCase() { } } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flush() = coroutines.drain(::pump) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt index 08ba02deb04..e805bdbd71d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.client.settings.rules.RulesConfigurable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.options.SearchableConfigurable @@ -42,12 +43,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() { assertFalse(cfg.isModified) } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun links(root: Container): List = buildList { for (comp in root.components) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialogTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialogTest.kt index 1fb21ed7a1a..b5bf5581fda 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialogTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialogTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.cli.KiloCliParser import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.settings.base.SettingsRow @@ -101,10 +102,5 @@ class AgentCreateDialogTest : BasePlatformTestCase() { return out } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentEditDialogTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentEditDialogTest.kt index 6e97b390c4f..6bae9e793f5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentEditDialogTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentEditDialogTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.cli.KiloCliParser import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle @@ -232,10 +233,5 @@ class AgentEditDialogTest : BasePlatformTestCase() { return out } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt index ec332464cb1..49453b91d7c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.cli.KiloCliParser import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService @@ -498,12 +499,7 @@ class AgentsSettingsUiTest : BasePlatformTestCase() { MouseEvent.BUTTON1, ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(300) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpEditDialogTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpEditDialogTest.kt index 853fda2d11d..e61524dce0b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpEditDialogTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpEditDialogTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.settings.base.SettingsRow import ai.kilocode.client.settings.base.SettingsStackedRow @@ -197,10 +198,5 @@ class McpEditDialogTest : BasePlatformTestCase() { return out } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpSettingsUiTest.kt index 2c69e9caece..aa4a216ee30 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/McpSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle @@ -430,12 +431,7 @@ class McpSettingsUiTest : BasePlatformTestCase() { MouseEvent.BUTTON1, ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(300) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt index 195f4da1150..b6448a4b11f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService @@ -583,12 +584,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { MouseEvent.BUTTON1, ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(300) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt index ffad41f3b9b..911846f530f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.autoapprove +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.testing.FakeAppRpcApi @@ -433,12 +434,7 @@ class AutoApproveSettingsUiTest : BasePlatformTestCase() { combo.selectedItem = item } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(200) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt index e6a0a780ee7..c29450cef00 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.autoapprove +import ai.kilocode.client.util.edtWait import ai.kilocode.client.testing.fire import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.list.ActiveListItem @@ -336,10 +337,5 @@ class SettingsInlineListTest : BasePlatformTestCase() { return out } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiTest.kt index 63acec830f5..27309a03030 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.base +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.testing.FakeAppRpcApi @@ -172,12 +173,7 @@ class BaseSettingsUiTest : BasePlatformTestCase() { edt { UIUtil.dispatchAllInvocationEvents() } } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun text(root: Container): String { val out = mutableListOf() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt index 2f0dfbeb1b5..207c1892e43 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.base +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.testing.FakeAppRpcApi @@ -102,12 +103,7 @@ class BaseSettingsUiWorkspaceTest : BasePlatformTestCase() { config = ConfigDto(model = model), ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(20) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurableTest.kt index a5b188afd54..0cc2536955f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurableTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.base +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.testing.FakeAppRpcApi @@ -190,12 +191,7 @@ class KiloReadyConfigurableTest : BasePlatformTestCase() { assertTrue(done()) } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun text(root: Container): String { val out = mutableListOf() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index a285d5be084..53afb653015 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.base +import ai.kilocode.client.util.edtWait import ai.kilocode.client.testing.fire import ai.kilocode.client.session.ui.PickerRow import ai.kilocode.client.ui.FilledBadgeIcon @@ -856,10 +857,5 @@ class SettingsListViewTest : BasePlatformTestCase() { false, ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt index 80c5a2e1c4d..acfbb5ae008 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.context +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.settings.base.SettingsToggle @@ -284,12 +285,7 @@ class ContextSettingsUiTest : BasePlatformTestCase() { .filterIsInstance() .single { it.toolTipText == tip } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(200) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUiTest.kt index 3401d0775bd..042dfe01a30 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.models +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.session.ui.model.ModelPicker @@ -365,12 +366,7 @@ class ModelsSettingsUiTest : BasePlatformTestCase() { private fun requireUi(): ModelsSettingsUi = requireNotNull(ui) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(200) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt index 8aa7bbb3234..69911d94967 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.providers +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloProviderService import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.testing.FakeProviderRpcApi @@ -1290,12 +1291,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { return out.joinToString("\n") } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) = runBlocking { repeat(200) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt index 06983d1c506..e4e13bafdf9 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.rules +import ai.kilocode.client.util.edtWait import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService @@ -358,12 +359,7 @@ class RulesSettingsUiTest : BasePlatformTestCase() { MouseEvent.BUTTON1, ) - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private fun flushUntil(done: () -> Boolean) { repeat(200) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt index 13c6d7ede54..e3cb9abacce 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.testing +import ai.kilocode.client.util.edtWait import ai.kilocode.client.util.UiTimer import ai.kilocode.client.util.UiTimerSource import com.intellij.openapi.application.ApplicationManager @@ -28,14 +29,7 @@ class TestUiTimers : UiTimerSource { } } - private fun edt(action: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { - action() - return - } - app.invokeAndWait(action) - } + private fun edt(action: () -> Unit) = edtWait(action) private inner class TestTimer( private val ms: Int, diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListMatchTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListMatchTest.kt new file mode 100644 index 00000000000..aaf448615dd --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListMatchTest.kt @@ -0,0 +1,34 @@ +package ai.kilocode.client.ui.list + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.measureTime + +class ActiveListMatchTest { + @Test + fun `matches words and camel case acronyms`() { + assertTrue(activeListMatches("wc", item("WorktreeController"))) + assertTrue(activeListMatches("ws ses", item("Worktree Session Editor"))) + assertTrue(activeListMatches("feature", item("feature-branch"))) + assertFalse(activeListMatches("zz", item("WorktreeController"))) + } + + @Test + fun `matcher handles repeated ambiguous prefixes quickly`() { + val text = List(28) { "aaaaaaaaaa" }.joinToString("-") + val query = "aaaaaaaaaab" + + val duration = measureTime { + assertFalse(activeListMatches(query, item(text))) + } + + assertTrue(duration < 200.milliseconds, "ambiguous match took $duration") + } + + private fun item(title: String) = object : ActiveListItem { + override val key = title + override val title = title + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/picker/PickerPopupTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/picker/PickerPopupTest.kt index 1a6181d01a3..804321e43b4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/picker/PickerPopupTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/picker/PickerPopupTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.ui.picker +import ai.kilocode.client.util.edtWait import com.intellij.CommonBundle import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.application.ApplicationManager @@ -69,12 +70,7 @@ class PickerPopupTest : BasePlatformTestCase() { return out } - private fun edt(block: () -> T): T { - var result: T? = null - ApplicationManager.getApplication().invokeAndWait { result = block() } - @Suppress("UNCHECKED_CAST") - return result as T - } + private fun edt(block: () -> T): T = edtWait(block) private class TestRenderer(model: CollectionListModel) : PickerListRenderer( model = model,