From ebeb65ad77ce93008008e1de11f5e1267dd42dfc Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 2 Jun 2026 17:26:49 -0400 Subject: [PATCH] refactor(kilo-jetbrains): unify settings base lifecycle --- .../client/settings/base/BaseSettingsUi.kt | 115 +++++++++++++++- .../settings/base/BaseWorkspaceSettingsUi.kt | 127 ------------------ .../settings/models/ModelsSettingsUi.kt | 4 +- .../settings/base/BaseSettingsUiTest.kt | 27 +++- ...Test.kt => BaseSettingsUiWorkspaceTest.kt} | 6 +- 5 files changed, 141 insertions(+), 138 deletions(-) delete mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUi.kt rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/{BaseWorkspaceSettingsUiTest.kt => BaseSettingsUiWorkspaceTest.kt} (97%) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt index ccafc479990..727cf0b0e85 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt @@ -1,25 +1,40 @@ package ai.kilocode.client.settings.base import ai.kilocode.client.KiloNotifications +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.settings.profile.UserProfileConfigurable +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto +import ai.kilocode.rpc.dto.ModelStateDto import com.intellij.ide.DataManager import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ConfigurableWithId import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.options.ex.Settings import com.intellij.openapi.project.ProjectManager +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.function.Predicate import javax.swing.JComponent -internal abstract class BaseSettingsUi( +internal abstract class BaseSettingsUi( protected val scope: CoroutineScope, initial: D, + private val app: KiloAppService = service(), + private val workspaces: KiloWorkspaceService = service(), + private val hint: String? = null, private val loginBanner: Boolean = true, ) : SettingsPanel() { protected lateinit var form: C @@ -28,6 +43,17 @@ internal abstract class BaseSettingsUi( protected var draft = initial protected val saving get() = save protected val saveError get() = error + protected var appState: KiloAppStateDto = app.state.value + private set + protected var modelState: ModelStateDto = app.models.value + private set + protected var projectDirectory: String? = null + private set + protected val hasProjectDirectory get() = projectDirectory != null || hint != null + protected var workspaceLoading = false + private set + protected var workspaceLoaded = false + private set private var baseline = initial private var pending: D? = null @@ -35,9 +61,72 @@ internal abstract class BaseSettingsUi( private var error: String? = null private var disposed = false - protected fun setSettingsContent(content: C) { + @RequiresEdt + protected fun startSettings(content: C) { form = content setContent(content) + syncContent() + start() + } + + private fun start() { + jobs += scope.launch { + app.state.collect { state -> withContext(edt) { updateApp(state) } } + } + jobs += scope.launch { + app.models.collect { state -> withContext(edt) { updateModels(state) } } + } + jobs += scope.launch { app.connect() } + val path = hint ?: return + jobs += scope.launch { + val dir = workspaces.resolveProjectDirectory(path) + withContext(edt) { + projectDirectory = dir + workspaceLoaded = false + syncContent() + load() + } + } + } + + @RequiresEdt + private fun updateApp(state: KiloAppStateDto) { + appState = state + if (state.status != KiloAppStatusDto.READY) { + workspaceLoading = false + unavailable(state) + syncContent() + return + } + acceptBase(draft(state)) + syncContent() + load() + } + + @RequiresEdt + private fun updateModels(state: ModelStateDto) { + modelState = state + models(state) + syncContent() + } + + @RequiresEdt + private fun load() { + val root = projectDirectory ?: return + if (appState.status != KiloAppStatusDto.READY || workspaceLoading || workspaceLoaded) return + workspaceLoading = true + clearWorkspaceError() + syncContent() + jobs += scope.launch { + val state = loadWorkspace(root) + withContext(edt) { + applyWorkspace(state) + workspaceLoaded = true + workspaceLoading = false + acceptBase(draft(appState)) + syncContent() + } + } } @RequiresEdt @@ -167,6 +256,15 @@ internal abstract class BaseSettingsUi( @RequiresEdt protected abstract fun failedText(): String + @RequiresEdt + protected abstract fun draft(state: KiloAppStateDto): D + + @RequiresBackgroundThread + protected abstract suspend fun loadWorkspace(root: String): W + + @RequiresEdt + protected abstract fun applyWorkspace(result: W) + @RequiresEdt protected open fun saved(base: D, draft: D): Boolean = base == draft @@ -188,6 +286,15 @@ internal abstract class BaseSettingsUi( @RequiresEdt protected open fun logSaveCompletedAfterDispose(change: P) = Unit + @RequiresEdt + protected open fun unavailable(state: KiloAppStateDto) = Unit + + @RequiresEdt + protected open fun models(state: ModelStateDto) = Unit + + @RequiresEdt + protected open fun clearWorkspaceError() = Unit + private fun openProfile(src: JComponent) { val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) if (settings != null) { @@ -207,4 +314,8 @@ internal abstract class BaseSettingsUi( { cfg: Configurable -> cfg.focusOn(UserProfileConfigurable.FOCUS_ACCOUNT_COMBO) }, ) } + + private companion object { + val edt = Dispatchers.EDT + ModalityState.any().asContextElement() + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUi.kt deleted file mode 100644 index 17c1c4f92ee..00000000000 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUi.kt +++ /dev/null @@ -1,127 +0,0 @@ -package ai.kilocode.client.settings.base - -import ai.kilocode.client.app.KiloAppService -import ai.kilocode.client.app.KiloWorkspaceService -import ai.kilocode.rpc.dto.KiloAppStateDto -import ai.kilocode.rpc.dto.KiloAppStatusDto -import ai.kilocode.rpc.dto.ModelStateDto -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.components.service -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread -import com.intellij.util.concurrency.annotations.RequiresEdt -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -internal abstract class BaseWorkspaceSettingsUi( - cs: CoroutineScope, - initial: D, - private val app: KiloAppService = service(), - private val workspaces: KiloWorkspaceService = service(), - private val hint: String? = null, - loginBanner: Boolean = true, -) : BaseSettingsUi(cs, initial, loginBanner) { - protected var appState: KiloAppStateDto = app.state.value - private set - protected var modelState: ModelStateDto = app.models.value - private set - protected var projectDirectory: String? = null - private set - protected val hasProjectDirectory get() = projectDirectory != null || hint != null - protected var workspaceLoading = false - private set - protected var workspaceLoaded = false - private set - - @RequiresEdt - protected fun startSettings(content: C) { - setSettingsContent(content) - syncContent() - start() - } - - private fun start() { - jobs += scope.launch { - app.state.collect { state -> withContext(edt) { updateApp(state) } } - } - jobs += scope.launch { - app.models.collect { state -> withContext(edt) { updateModels(state) } } - } - jobs += scope.launch { app.connect() } - val path = hint ?: return - jobs += scope.launch { - val dir = workspaces.resolveProjectDirectory(path) - withContext(edt) { - projectDirectory = dir - workspaceLoaded = false - syncContent() - load() - } - } - } - - @RequiresEdt - private fun updateApp(state: KiloAppStateDto) { - appState = state - if (state.status != KiloAppStatusDto.READY) { - workspaceLoading = false - unavailable(state) - syncContent() - return - } - acceptBase(draft(state)) - syncContent() - load() - } - - @RequiresEdt - private fun updateModels(state: ModelStateDto) { - modelState = state - models(state) - syncContent() - } - - @RequiresEdt - private fun load() { - val root = projectDirectory ?: return - if (appState.status != KiloAppStatusDto.READY || workspaceLoading || workspaceLoaded) return - workspaceLoading = true - clearWorkspaceError() - syncContent() - jobs += scope.launch { - val state = loadWorkspace(root) - withContext(edt) { - applyWorkspace(state) - workspaceLoaded = true - workspaceLoading = false - acceptBase(draft(appState)) - syncContent() - } - } - } - - @RequiresEdt - protected abstract fun draft(state: KiloAppStateDto): D - - @RequiresBackgroundThread - protected abstract suspend fun loadWorkspace(root: String): W - - @RequiresEdt - protected abstract fun applyWorkspace(result: W) - - @RequiresEdt - protected open fun unavailable(state: KiloAppStateDto) = Unit - - @RequiresEdt - protected open fun models(state: ModelStateDto) = Unit - - @RequiresEdt - protected open fun clearWorkspaceError() = Unit - - private companion object { - val edt = Dispatchers.EDT + ModalityState.any().asContextElement() - } -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt index a48d410fbe1..322e5fcfcbb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt @@ -7,7 +7,7 @@ import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.model.ModelPicker import ai.kilocode.client.session.ui.model.ModelText import ai.kilocode.client.settings.base.BaseContentPanel -import ai.kilocode.client.settings.base.BaseWorkspaceSettingsUi +import ai.kilocode.client.settings.base.BaseSettingsUi import ai.kilocode.client.settings.base.SettingsBannerKind import ai.kilocode.client.settings.base.SettingsRow import ai.kilocode.client.settings.base.SettingsRows @@ -32,7 +32,7 @@ internal class ModelsSettingsUi( private val app: KiloAppService = service(), private val workspaces: KiloWorkspaceService = service(), directory: String? = null, -) : BaseWorkspaceSettingsUi( +) : BaseSettingsUi( cs, ModelsDraft(), app, 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 88a4f265394..4966673eb57 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,10 @@ package ai.kilocode.client.settings.base +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.testing.FakeAppRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.KiloAppStateDto import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.UIUtil @@ -14,11 +19,17 @@ import javax.swing.text.JTextComponent class BaseSettingsUiTest : BasePlatformTestCase() { private lateinit var scope: CoroutineScope + private lateinit var appScope: CoroutineScope + private lateinit var app: KiloAppService + private lateinit var workspaces: KiloWorkspaceService private var panel: FakePanel? = null override fun setUp() { super.setUp() scope = CoroutineScope(SupervisorJob()) + appScope = CoroutineScope(SupervisorJob()) + app = KiloAppService(appScope, FakeAppRpcApi()) + workspaces = KiloWorkspaceService(appScope, FakeWorkspaceRpcApi()) } override fun tearDown() { @@ -27,6 +38,7 @@ class BaseSettingsUiTest : BasePlatformTestCase() { if (view != null) edt { view.dispose() } panel = null scope.cancel() + appScope.cancel() } finally { super.tearDown() } @@ -137,7 +149,7 @@ class BaseSettingsUiTest : BasePlatformTestCase() { } private fun create(login: Boolean = true): FakePanel { - val view = edt { FakePanel(scope, login) } + val view = edt { FakePanel(scope, app, workspaces, login) } panel = view return view } @@ -181,15 +193,16 @@ class BaseSettingsUiTest : BasePlatformTestCase() { private class FakePanel( cs: CoroutineScope, + app: KiloAppService, + workspaces: KiloWorkspaceService, login: Boolean, - ) : BaseSettingsUi(cs, Draft("old"), login) { + ) : BaseSettingsUi(cs, Draft("old"), app, workspaces, loginBanner = login) { private val callbacks = mutableListOf<(Draft?) -> Unit>() var disposedFailures = 0 private set init { - setSettingsContent(FakeContent()) - syncContent() + startSettings(FakeContent()) } fun edit(value: String) = updateDraft { copy(value = value) } @@ -210,6 +223,12 @@ class BaseSettingsUiTest : BasePlatformTestCase() { override fun base(result: Draft): Draft = result + override fun draft(state: KiloAppStateDto): Draft = draft + + override suspend fun loadWorkspace(root: String) = Unit + + override fun applyWorkspace(result: Unit) = Unit + override fun syncContent() { val err = saveError if (saving) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt similarity index 97% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUiTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt index f588880117b..c1a7070266a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseWorkspaceSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -class BaseWorkspaceSettingsUiTest : BasePlatformTestCase() { +class BaseSettingsUiWorkspaceTest : BasePlatformTestCase() { private lateinit var appScope: CoroutineScope private lateinit var uiScope: CoroutineScope private lateinit var rpc: FakeAppRpcApi @@ -54,7 +54,7 @@ class BaseWorkspaceSettingsUiTest : BasePlatformTestCase() { workspaceRpc.directory = "/resolved" val view = create("/hint") - flushUntil { edt { view.value() == "new" && view.roots == listOf("/resolved") } } + flushUntil { edt { view.value() == "new" && view.roots == listOf("/resolved") && view.loaded() } } edt { assertEquals("new", view.value()) @@ -128,7 +128,7 @@ class BaseWorkspaceSettingsUiTest : BasePlatformTestCase() { app: KiloAppService, workspaces: KiloWorkspaceService, hint: String, - ) : BaseWorkspaceSettingsUi( + ) : BaseSettingsUi( cs, Draft("old"), app,