From 4a6ddb5d3a0fee65e47b8ddd83290de556b87433 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 25 May 2026 15:10:55 -0400 Subject: [PATCH] fix(jetbrains): finish legacy migration progress --- .../jetbrains-migration-autocomplete.md | 5 ++ .changeset/jetbrains-migration-legacy-file.md | 5 ++ .../KiloBackendLegacyMigrationStoreService.kt | 9 +++ .../migration/LegacyMigrationEngine.kt | 23 ++++-- .../migration/LegacyMigrationModels.kt | 2 + .../backend/rpc/KiloMigrationRpcApiImpl.kt | 4 +- .../backend/rpc/MigrationRpcMapper.kt | 2 + .../LegacyMigrationOrchestrationTest.kt | 73 ++++++++++++++++++- .../migration/LegacySettingsFileFixture.kt | 2 + .../KiloAutocompleteSettingsService.kt | 40 ++++++++++ .../client/migration/KiloMigrationService.kt | 42 ++++++++++- .../migration/MigrationSelectionBuilder.kt | 3 + .../client/migration/MigrationUiState.kt | 1 + .../resources/kilo.jetbrains.frontend.xml | 1 + .../migration/KiloMigrationServiceTest.kt | 61 +++++++++++++++- .../ai/kilocode/rpc/dto/MigrationDto.kt | 2 + 16 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 .changeset/jetbrains-migration-autocomplete.md create mode 100644 .changeset/jetbrains-migration-legacy-file.md create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/autocomplete/KiloAutocompleteSettingsService.kt diff --git a/.changeset/jetbrains-migration-autocomplete.md b/.changeset/jetbrains-migration-autocomplete.md new file mode 100644 index 00000000000..1c79fd7b19c --- /dev/null +++ b/.changeset/jetbrains-migration-autocomplete.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Migrate JetBrains autocomplete settings and finish skipped chat history migration progress. diff --git a/.changeset/jetbrains-migration-legacy-file.md b/.changeset/jetbrains-migration-legacy-file.md new file mode 100644 index 00000000000..87e0e17a3d6 --- /dev/null +++ b/.changeset/jetbrains-migration-legacy-file.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Open the migration flow directly on settings selection and support removing the legacy settings file after migration. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/KiloBackendLegacyMigrationStoreService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/KiloBackendLegacyMigrationStoreService.kt index 64e48960e5f..8952b416cdc 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/KiloBackendLegacyMigrationStoreService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/KiloBackendLegacyMigrationStoreService.kt @@ -68,6 +68,15 @@ class LegacySettingsFileMigrationStore( override fun cleanup(targets: LegacyCleanupTargets): LegacyCleanupReport { val root = read()?.toMutableMap() ?: return LegacyCleanupReport(cleaned = emptyList(), errors = emptyList()) + if (targets.legacySettingsFile) { + val err = runCatching { + if (file.delete()) null else "Failed to delete ${file.absolutePath}" + }.getOrElse { it.message ?: "Failed to delete ${file.absolutePath}" } + return LegacyCleanupReport( + cleaned = if (err == null) listOf("legacySettingsFile") else emptyList(), + errors = listOfNotNull(err), + ) + } val cleaned = mutableListOf() if (targets.providerProfiles && root.remove("providerProfiles") != null) cleaned.add("providerProfiles") if (targets.mcpSettings && root.remove("mcpSettings") != null) cleaned.add("mcpSettings") diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationEngine.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationEngine.kt index 5f1087ddd3b..93f98875db1 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationEngine.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationEngine.kt @@ -218,15 +218,19 @@ class LegacyMigrationEngine( // Check if already exists (unless force) if (!sel.force && backend.sessionExists(sessionId)) { + val msg = "Session already exists, skipped" val progress = LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.skipped) sink.session(progress) - results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.success, "Session already exists, skipped")) + results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.success, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.success, msg)) continue } if (conversationRaw == null) { - sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.error, "Conversation file not found")) - results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.error, "Conversation file not found")) + val msg = "Conversation file not found" + sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.error, msg)) + results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.error, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.error, msg)) continue } @@ -238,6 +242,7 @@ class LegacyMigrationEngine( val msg = e.message ?: "Parse error" sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.error, msg)) results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.error, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.error, msg)) null } ?: continue @@ -247,6 +252,7 @@ class LegacyMigrationEngine( val msg = e.message ?: "Project import failed" sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.error, msg)) results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.error, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.error, msg)) null } ?: continue @@ -267,12 +273,15 @@ class LegacyMigrationEngine( val msg = e.message ?: "Session import failed" sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.error, msg)) results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.error, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.error, msg)) null } ?: continue if (importResult.skipped) { + val msg = "Session already exists, skipped" sink.session(LegacyMigrationSessionProgress(info, idx, selections.sessions.size, MigrationSessionPhase.skipped)) - results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.success, "Session already exists, skipped")) + results.add(LegacyMigrationResultItem(sel.id, MigrationItemCategory.session, MigrationItemStatus.success, msg)) + sink.item(LegacyMigrationItemProgress(sel.id, MigrationItemProgressStatus.success, msg)) continue } @@ -339,11 +348,11 @@ class LegacyMigrationEngine( } } - // Autocomplete — report skipped/warning (no JetBrains equivalent known yet) + // Autocomplete settings are persisted by the JetBrains frontend before backend migration starts. if (selections.settings.autocomplete && settings.autocomplete != null) { sink.item(LegacyMigrationItemProgress("Autocomplete settings", MigrationItemProgressStatus.migrating)) - results.add(LegacyMigrationResultItem("Autocomplete settings", MigrationItemCategory.settings, MigrationItemStatus.warning, "Autocomplete settings migration is not yet available for JetBrains")) - sink.item(LegacyMigrationItemProgress("Autocomplete settings", MigrationItemProgressStatus.warning, "Autocomplete settings migration is not yet available for JetBrains")) + results.add(LegacyMigrationResultItem("Autocomplete settings", MigrationItemCategory.settings, MigrationItemStatus.success)) + sink.item(LegacyMigrationItemProgress("Autocomplete settings", MigrationItemProgressStatus.success)) } return LegacyMigrationReport(results) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationModels.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationModels.kt index 734f175edd8..25025e7ca6d 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationModels.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/migration/LegacyMigrationModels.kt @@ -155,6 +155,7 @@ data class LegacyMigrationSelections( val sessions: List, val defaultModel: Boolean, val settings: MigrationSettingsSelections, + val keepLegacySettingsFile: Boolean = true, ) // --------------------------------------------------------------------------- @@ -219,6 +220,7 @@ data class LegacyCleanupTargets( val customModes: Boolean = false, val globalState: Boolean = false, val taskHistory: Boolean = false, + val legacySettingsFile: Boolean = false, ) data class LegacyCleanupReport( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloMigrationRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloMigrationRpcApiImpl.kt index a5a0f022436..0853ba8d0d4 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloMigrationRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloMigrationRpcApiImpl.kt @@ -113,7 +113,7 @@ class KiloMigrationRpcApiImpl : KiloMigrationRpcApi { } override suspend fun cleanup(targets: LegacyCleanupTargetsDto): LegacyCleanupReportDto { - LOG.info("Migration RPC cleanup: providerProfiles=${targets.providerProfiles} mcp=${targets.mcpSettings} modes=${targets.customModes} state=${targets.globalState} history=${targets.taskHistory}") + LOG.info("Migration RPC cleanup: providerProfiles=${targets.providerProfiles} mcp=${targets.mcpSettings} modes=${targets.customModes} state=${targets.globalState} history=${targets.taskHistory} file=${targets.legacySettingsFile}") val mgr = manager() val store = storeService.store() val report = withContext(Dispatchers.IO) { mgr.cleanup(store, MigrationRpcMapper.fromDto(targets)) } @@ -122,5 +122,5 @@ class KiloMigrationRpcApiImpl : KiloMigrationRpcApi { } private fun selectionSummary(selections: LegacyMigrationSelectionsDto): String = - "providers=${selections.providers.size} mcp=${selections.mcpServers.size} modes=${selections.customModes.size} sessions=${selections.sessions.size} model=${selections.defaultModel} settings=true" + "providers=${selections.providers.size} mcp=${selections.mcpServers.size} modes=${selections.customModes.size} sessions=${selections.sessions.size} model=${selections.defaultModel} settings=true keepFile=${selections.keepLegacySettingsFile}" } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/MigrationRpcMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/MigrationRpcMapper.kt index e9ab75a564f..51742e1365e 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/MigrationRpcMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/MigrationRpcMapper.kt @@ -135,6 +135,7 @@ internal object MigrationRpcMapper { sessions = dto.sessions.map(::fromDto), defaultModel = dto.defaultModel, settings = fromDto(dto.settings), + keepLegacySettingsFile = dto.keepLegacySettingsFile, ) private fun fromDto(dto: MigrationSessionSelectionDto): MigrationSessionSelection = @@ -223,6 +224,7 @@ internal object MigrationRpcMapper { customModes = dto.customModes, globalState = dto.globalState, taskHistory = dto.taskHistory, + legacySettingsFile = dto.legacySettingsFile, ) fun toDto(r: LegacyCleanupReport): LegacyCleanupReportDto = diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacyMigrationOrchestrationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacyMigrationOrchestrationTest.kt index ed6fa40505f..2814d38ad24 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacyMigrationOrchestrationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacyMigrationOrchestrationTest.kt @@ -1,9 +1,11 @@ package ai.kilocode.backend.migration +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue /** @@ -142,10 +144,13 @@ class LegacyMigrationOrchestrationTest { val sessionId = ai.kilocode.backend.migration.session.LegacySessionIds.createSessionId("t1") backend.existingSessionIds = setOf(sessionId) val sel = noSelections().copy(sessions = listOf(MigrationSessionSelection("t1", force = false))) - val report = eng.migrate(sel) + val items = mutableListOf() + val report = eng.migrate(sel, itemSink(items)) val item = report.items.find { it.category == MigrationItemCategory.session } assertNotNull(item) assertTrue(item!!.message?.contains("skipped", ignoreCase = true) == true) + assertEquals(listOf(MigrationItemProgressStatus.migrating, MigrationItemProgressStatus.success), items.map { it.status }) + assertTrue(items[1].message?.contains("skipped", ignoreCase = true) == true) assertEquals(0, backend.projectCalls.size) } @@ -173,9 +178,42 @@ class LegacyMigrationOrchestrationTest { } backend.sessionImportSkipped = true val sel = noSelections().copy(sessions = listOf(MigrationSessionSelection("t1"))) - eng.migrate(sel) + val items = mutableListOf() + eng.migrate(sel, itemSink(items)) assertEquals(0, backend.messageCalls.size) assertEquals(0, backend.partCalls.size) + assertEquals(listOf(MigrationItemProgressStatus.migrating, MigrationItemProgressStatus.success), items.map { it.status }) + assertTrue(items[1].message?.contains("skipped", ignoreCase = true) == true) + } + + @Test + fun `migrate - missing session conversation emits terminal error progress`() { + val (eng, _, _) = setup { + taskHistory = """[{"id":"t1","task":"Test"}]""" + } + val sel = noSelections().copy(sessions = listOf(MigrationSessionSelection("t1"))) + val items = mutableListOf() + eng.migrate(sel, itemSink(items)) + assertEquals(listOf(MigrationItemProgressStatus.migrating, MigrationItemProgressStatus.error), items.map { it.status }) + assertEquals("Conversation file not found", items[1].message) + } + + @Test + fun `migrate - autocomplete settings report success`() { + val (eng, _, _) = setup { + globalState["ghostServiceSettings"] = kotlinx.serialization.json.JsonObject( + mapOf( + "enableAutoTrigger" to JsonPrimitive(true), + "enableSmartInlineTaskKeybinding" to JsonPrimitive(true), + "enableChatAutocomplete" to JsonPrimitive(true), + ) + ) + } + val sel = noSelections().copy(settings = noSelections().settings.copy(autocomplete = true)) + val report = eng.migrate(sel) + val item = report.items.single { it.item == "Autocomplete settings" } + assertEquals(MigrationItemStatus.success, item.status) + assertEquals(null, item.message) } // ----------------------------------------------------------------------- @@ -209,9 +247,40 @@ class LegacyMigrationOrchestrationTest { assertTrue(report.hasErrors) } + // ----------------------------------------------------------------------- + // Cleanup + // ----------------------------------------------------------------------- + + @Test + fun `cleanup - legacy settings file target deletes file`() { + val (eng, fixture, _) = setup { + providerProfiles = """{"currentApiConfigName":"p","apiConfigs":{}}""" + } + val report = eng.cleanup(LegacyCleanupTargets(legacySettingsFile = true)) + assertEquals(listOf("legacySettingsFile"), report.cleaned) + assertEquals(emptyList(), report.errors) + assertFalse(fixture.exists()) + } + + @Test + fun `cleanup - data target preserves legacy settings file`() { + val (eng, fixture, _) = setup { + providerProfiles = """{"currentApiConfigName":"p","apiConfigs":{}}""" + } + val report = eng.cleanup(LegacyCleanupTargets(providerProfiles = true)) + assertEquals(listOf("providerProfiles"), report.cleaned) + assertEquals(emptyList(), report.errors) + assertTrue(fixture.exists()) + } + // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- private fun assertNotNull(actual: Any?) = kotlin.test.assertNotNull(actual) + + private fun itemSink(items: MutableList) = object : LegacyMigrationSink { + override fun item(progress: LegacyMigrationItemProgress) { items.add(progress) } + override fun session(progress: LegacyMigrationSessionProgress) = Unit + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacySettingsFileFixture.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacySettingsFileFixture.kt index 003f3ea1fc0..81e1fe78b66 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacySettingsFileFixture.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/migration/LegacySettingsFileFixture.kt @@ -28,6 +28,8 @@ internal class LegacySettingsFileFixture { migrationStatus = LegacySettingsFileMigrationStore(file).status() } + fun exists() = file.exists() + private fun flush() { val root = mutableMapOf() migrationStatus?.let { root["migrationStatus"] = JsonPrimitive(it.name) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/autocomplete/KiloAutocompleteSettingsService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/autocomplete/KiloAutocompleteSettingsService.kt new file mode 100644 index 00000000000..81f2762fe06 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/autocomplete/KiloAutocompleteSettingsService.kt @@ -0,0 +1,40 @@ +package ai.kilocode.client.autocomplete + +import ai.kilocode.rpc.dto.LegacyAutocompleteSettingsDto +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@Service(Service.Level.APP) +@State( + name = "KiloAutocompleteSettings", + storages = [Storage("kiloAutocompleteSettings.xml")], +) +class KiloAutocompleteSettingsService : PersistentStateComponent { + + data class State( + var enableAutoTrigger: Boolean? = null, + var enableSmartInlineTaskKeybinding: Boolean? = null, + var enableChatAutocomplete: Boolean? = null, + ) + + private var state = State() + + override fun getState(): State = state + + override fun loadState(state: State) { + this.state = state + } + + fun applyLegacy(settings: LegacyAutocompleteSettingsDto) { + settings.enableAutoTrigger?.let { state.enableAutoTrigger = it } + settings.enableSmartInlineTaskKeybinding?.let { state.enableSmartInlineTaskKeybinding = it } + settings.enableChatAutocomplete?.let { state.enableChatAutocomplete = it } + } + + companion object { + fun getInstance(): KiloAutocompleteSettingsService = service() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt index 8e765a10411..5cc7364f71c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt @@ -3,10 +3,13 @@ package ai.kilocode.client.migration import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.autocomplete.KiloAutocompleteSettingsService import ai.kilocode.log.KiloLog import ai.kilocode.rpc.KiloMigrationRpcApi import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto +import ai.kilocode.rpc.dto.LegacyAutocompleteSettingsDto +import ai.kilocode.rpc.dto.LegacyCleanupTargetsDto import ai.kilocode.rpc.dto.LegacyMigrationEventDto import ai.kilocode.rpc.dto.LegacyMigrationResultItemDto import ai.kilocode.rpc.dto.LegacyMigrationStatusDto @@ -49,12 +52,19 @@ class KiloMigrationService internal constructor( private val cs: CoroutineScope, private val rpc: KiloMigrationRpcApi?, appState: StateFlow?, + private val autocomplete: ((LegacyAutocompleteSettingsDto) -> Unit)?, ) : MigrationUiController { /** Platform constructor — resolves RPC lazily. */ - constructor(cs: CoroutineScope) : this(cs, null, service().state) + constructor(cs: CoroutineScope) : this(cs, null, service().state, null) - internal constructor(cs: CoroutineScope, rpc: KiloMigrationRpcApi?) : this(cs, rpc, null) + internal constructor(cs: CoroutineScope, rpc: KiloMigrationRpcApi?) : this(cs, rpc, null, null) + + internal constructor( + cs: CoroutineScope, + rpc: KiloMigrationRpcApi?, + appState: StateFlow?, + ) : this(cs, rpc, appState, null) companion object { private val LOG = KiloLog.create(KiloMigrationService::class.java) @@ -67,6 +77,7 @@ class KiloMigrationService internal constructor( private val migrating = AtomicBoolean(false) private val migrateJob = AtomicReference(null) + private val lastSelections = AtomicReference(null) init { if (appState != null) { @@ -95,9 +106,11 @@ class KiloMigrationService internal constructor( return } LOG.info("Migration wizard: user started migration ${selectionSummary(selections)}") + lastSelections.set(selections) val dto = MigrationSelectionBuilder.toDto(selections) val initialProgress = buildInitialProgress(selections, current.detection) + applyAutocomplete(selections, current.detection) _state.value = current.copy( phase = MigrationUiPhase.migrating, @@ -194,10 +207,14 @@ class KiloMigrationService internal constructor( } val hasErrors = current.results.any { it.status == MigrationItemStatusDto.error } val status = if (hasErrors) LegacyMigrationStatusDto.completed_with_errors else LegacyMigrationStatusDto.completed + val selections = lastSelections.get() LOG.info("Migration wizard: user finished migration status=$status results=${current.results.size} errors=${current.results.count { it.status == MigrationItemStatusDto.error }}") cs.launch { try { call { finalize(status) } + if (selections?.keepLegacySettingsFile == false) { + call { cleanup(cleanupTargets()) } + } } catch (e: Exception) { LOG.warn("migration finalize failed", e) } @@ -304,8 +321,27 @@ class KiloMigrationService internal constructor( ) } + private fun applyAutocomplete( + selections: MigrationUiSelections, + detection: ai.kilocode.rpc.dto.LegacyMigrationDetectionDto, + ) { + if (!selections.settings.autocomplete) return + val settings = detection.settings?.autocomplete ?: return + val apply = autocomplete ?: { KiloAutocompleteSettingsService.getInstance().applyLegacy(it) } + apply(settings) + } + private fun selectionSummary(selections: MigrationUiSelections): String = - "providers=${selections.providers.size}:${selections.providers.joinToString(",")} mcp=${selections.mcpServers.size}:${selections.mcpServers.joinToString(",")} modes=${selections.customModes.size}:${selections.customModes.joinToString(",")} sessions=${selections.sessions.size} model=${selections.defaultModel} settings=${settingsSummary(selections.settings)}" + "providers=${selections.providers.size}:${selections.providers.joinToString(",")} mcp=${selections.mcpServers.size}:${selections.mcpServers.joinToString(",")} modes=${selections.customModes.size}:${selections.customModes.joinToString(",")} sessions=${selections.sessions.size} model=${selections.defaultModel} settings=${settingsSummary(selections.settings)} keepFile=${selections.keepLegacySettingsFile}" + + private fun cleanupTargets() = LegacyCleanupTargetsDto( + providerProfiles = true, + mcpSettings = true, + customModes = true, + globalState = true, + taskHistory = true, + legacySettingsFile = true, + ) private fun settingsSummary(settings: MigrationSettingsUiSelections): String = "commandRules=${settings.autoApproval.commandRules},read=${settings.autoApproval.readPermission},write=${settings.autoApproval.writePermission},execute=${settings.autoApproval.executePermission},mcp=${settings.autoApproval.mcpPermission},task=${settings.autoApproval.taskPermission},language=${settings.language},autocomplete=${settings.autocomplete}" diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationSelectionBuilder.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationSelectionBuilder.kt index 15c96b276a9..38b08cc0bd6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationSelectionBuilder.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationSelectionBuilder.kt @@ -56,6 +56,7 @@ object MigrationSelectionBuilder { sessions = sessions, defaultModel = defaultModel, settings = settingsSel, + keepLegacySettingsFile = true, ) } @@ -83,6 +84,7 @@ object MigrationSelectionBuilder { language = selections.settings.language, autocomplete = selections.settings.autocomplete, ), + keepLegacySettingsFile = selections.keepLegacySettingsFile, ) /** @@ -106,5 +108,6 @@ object MigrationSelectionBuilder { language = false, autocomplete = false, ), + keepLegacySettingsFile = true, ) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationUiState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationUiState.kt index 44d2ae9fff5..f672b241eef 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationUiState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/MigrationUiState.kt @@ -34,6 +34,7 @@ data class MigrationUiSelections( val sessions: List = emptyList(), val defaultModel: Boolean = false, val settings: MigrationSettingsUiSelections = MigrationSettingsUiSelections(), + val keepLegacySettingsFile: Boolean = true, ) // --------------------------------------------------------------------------- diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index 080953e15f5..8e694c76427 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -8,6 +8,7 @@ + + private val autocomplete = mutableListOf() override fun setUp() { super.setUp() scope = CoroutineScope(SupervisorJob()) rpc = FakeMigrationRpcApi() app = MutableStateFlow(KiloAppStateDto(KiloAppStatusDto.DISCONNECTED)) - service = KiloMigrationService(scope, rpc, app) + autocomplete.clear() + service = KiloMigrationService(scope, rpc, app) { autocomplete.add(it) } } override fun tearDown() { @@ -95,9 +99,30 @@ class KiloMigrationServiceTest : BasePlatformTestCase() { settle() assertEquals(1, rpc.finalizeCalls.size) assertEquals(LegacyMigrationStatusDto.completed, rpc.finalizeCalls[0]) + assertEquals(0, rpc.cleanupCalls.size) assertEquals(MigrationUiState.Hidden, service.state.value) } + fun `test finish after unchecked keep file cleans up legacy settings file`() { + app.value = KiloAppStateDto(KiloAppStatusDto.MIGRATION_REQUIRED, migration = sampleDetection()) + settle() + service.start(MigrationUiSelections(providers = listOf("profile1"), keepLegacySettingsFile = false)) + settle() + + service.finish() + settle() + + assertEquals(1, rpc.finalizeCalls.size) + assertEquals(1, rpc.cleanupCalls.size) + val targets = rpc.cleanupCalls[0] + assertTrue(targets.providerProfiles) + assertTrue(targets.mcpSettings) + assertTrue(targets.customModes) + assertTrue(targets.globalState) + assertTrue(targets.taskHistory) + assertTrue(targets.legacySettingsFile) + } + fun `test start emits migrating state and initial pending progress`() = runBlocking { app.value = KiloAppStateDto(KiloAppStatusDto.MIGRATION_REQUIRED, migration = sampleDetection()) delay(100) @@ -172,6 +197,40 @@ class KiloMigrationServiceTest : BasePlatformTestCase() { assertEquals(2, dto.sessions.size) assertTrue(dto.sessions.all { it.force }) assertEquals(listOf("ses_1", "ses_2"), dto.sessions.map { it.id }) + assertTrue(dto.keepLegacySettingsFile) + } + + fun `test start persists selected legacy autocomplete settings`() { + val detection = sampleDetection().copy( + settings = LegacySettingsDto( + autoApprovalEnabled = null, + allowedCommands = null, + deniedCommands = null, + alwaysAllowReadOnly = null, + alwaysAllowReadOnlyOutsideWorkspace = null, + alwaysAllowWrite = null, + alwaysAllowExecute = null, + alwaysAllowMcp = null, + alwaysAllowModeSwitch = null, + alwaysAllowSubtasks = null, + language = null, + autocomplete = LegacyAutocompleteSettingsDto( + enableAutoTrigger = true, + enableSmartInlineTaskKeybinding = false, + enableChatAutocomplete = true, + ), + ) + ) + app.value = KiloAppStateDto(KiloAppStatusDto.MIGRATION_REQUIRED, migration = detection) + settle() + + service.start(MigrationUiSelections(settings = MigrationSettingsUiSelections(autocomplete = true))) + settle() + + assertEquals(1, autocomplete.size) + assertEquals(true, autocomplete[0].enableAutoTrigger) + assertEquals(false, autocomplete[0].enableSmartInlineTaskKeybinding) + assertEquals(true, autocomplete[0].enableChatAutocomplete) } private fun sampleDetection() = LegacyMigrationDetectionDto( diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/MigrationDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/MigrationDto.kt index 10ef707c952..51045c3d531 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/MigrationDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/MigrationDto.kt @@ -125,6 +125,7 @@ data class LegacyMigrationSelectionsDto( val sessions: List, val defaultModel: Boolean, val settings: MigrationSettingsSelectionsDto, + val keepLegacySettingsFile: Boolean = true, ) // --------------------------------------------------------------------------- @@ -201,6 +202,7 @@ data class LegacyCleanupTargetsDto( val customModes: Boolean = false, val globalState: Boolean = false, val taskHistory: Boolean = false, + val legacySettingsFile: Boolean = false, ) @Serializable