mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
refactor(kilo-jetbrains): unify settings base lifecycle
This commit is contained in:
+113
-2
@@ -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<C : BaseContentPanel, D, P, R>(
|
||||
internal abstract class BaseSettingsUi<C : BaseContentPanel, D, P, R, W>(
|
||||
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<C : BaseContentPanel, D, P, R>(
|
||||
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<C : BaseContentPanel, D, P, R>(
|
||||
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<C : BaseContentPanel, D, P, R>(
|
||||
@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<C : BaseContentPanel, D, P, R>(
|
||||
@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<C : BaseContentPanel, D, P, R>(
|
||||
{ cfg: Configurable -> cfg.focusOn(UserProfileConfigurable.FOCUS_ACCOUNT_COMBO) },
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
|
||||
}
|
||||
}
|
||||
|
||||
-127
@@ -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<C : BaseContentPanel, D, P, R, W>(
|
||||
cs: CoroutineScope,
|
||||
initial: D,
|
||||
private val app: KiloAppService = service(),
|
||||
private val workspaces: KiloWorkspaceService = service(),
|
||||
private val hint: String? = null,
|
||||
loginBanner: Boolean = true,
|
||||
) : BaseSettingsUi<C, D, P, R>(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()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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<ModelsSettingsContent, ModelsDraft, ConfigPatchDto, KiloAppStateDto, ModelsWorkspaceDto>(
|
||||
) : BaseSettingsUi<ModelsSettingsContent, ModelsDraft, ConfigPatchDto, KiloAppStateDto, ModelsWorkspaceDto>(
|
||||
cs,
|
||||
ModelsDraft(),
|
||||
app,
|
||||
|
||||
+23
-4
@@ -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<FakeContent, Draft, Change, Draft>(cs, Draft("old"), login) {
|
||||
) : BaseSettingsUi<FakeContent, Draft, Change, Draft, Unit>(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) {
|
||||
|
||||
+3
-3
@@ -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<FakeContent, Draft, Change, Draft, String>(
|
||||
) : BaseSettingsUi<FakeContent, Draft, Change, Draft, String>(
|
||||
cs,
|
||||
Draft("old"),
|
||||
app,
|
||||
Reference in New Issue
Block a user