fix(jetbrains): make migration wizard modal

This commit is contained in:
kirillk
2026-05-25 12:04:01 -04:00
parent e4b610a4fc
commit c0ede175f3
5 changed files with 105 additions and 14 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Hide the JetBrains migration wizard behind a true modal overlay and restore its width after tool-window resizing.
@@ -127,6 +127,7 @@ class SessionUi(
private lateinit var prompt: PromptPanel
private lateinit var load: LoadingPanel
private lateinit var migrationOverlay: MigrationOverlayPanel
private var modalFocus: (() -> JComponent)? = null
private var style = SessionEditorStyle.current()
init {
@@ -159,13 +160,15 @@ class SessionUi(
internal fun currentStyle() = style
val defaultFocusedComponent: JComponent get() {
val state = migration.state.value
if (state !is MigrationUiState.Hidden && root.blocker.isVisible) {
return migrationOverlay.preferredFocusComponent()
}
modalFocus?.invoke()?.let { return it }
return prompt.defaultFocusedComponent
}
internal fun setModalContent(content: JComponent?, focus: (() -> JComponent)? = null) {
modalFocus = if (content == null) null else focus
root.setModalContent(content)
}
private fun buildUi() {
root = SessionRootPanel()
@@ -176,7 +179,12 @@ class SessionUi(
onStart = { sel -> migration.start(sel) }
onForce = { ids -> migration.force(ids) }
}
root.setBlocker(migrationOverlay)
migrationOverlay.border = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING),
)
account = SessionAccountOverlay(
select = { org -> controller.selectOrganization(org) },
@@ -336,12 +344,12 @@ class SessionUi(
when (state) {
is MigrationUiState.Hidden -> {
if (root.blocker.isVisible) LOG.info("Migration wizard: overlay hidden session=${id ?: cacheKey ?: "new"}")
root.setBlocked(false)
setModalContent(null)
}
is MigrationUiState.Needed -> {
if (!root.blocker.isVisible) LOG.info("Migration wizard: overlay shown session=${id ?: cacheKey ?: "new"} phase=${state.phase}")
migrationOverlay.update(state)
root.setBlocked(true)
setModalContent(migrationOverlay) { migrationOverlay.preferredFocusComponent() }
}
}
}
@@ -1,7 +1,12 @@
package ai.kilocode.client.session.ui
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.HAlign
import ai.kilocode.client.ui.layout.VAlign
import ai.kilocode.client.ui.layout.align
import com.intellij.util.ui.JBDimension
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Rectangle
import javax.swing.JComponent
@@ -31,11 +36,15 @@ class SessionRootPanel : JLayeredPane() {
overlay.addOverlay(child, bounds)
}
fun setBlocker(child: JComponent) {
fun setModalContent(child: JComponent?) {
blocker.removeAll()
blocker.add(child)
if (child != null) blocker.add(child.align(HAlign.CENTER, VAlign.CENTER), BorderLayout.CENTER)
blocker.isVisible = child != null
if (child != null) blocker.requestFocusInWindow()
blocker.revalidate()
blocker.repaint()
revalidate()
repaint()
}
fun setBlocked(value: Boolean) {
@@ -105,14 +114,24 @@ class SessionRootPanel : JLayeredPane() {
*/
class Blocker : JPanel() {
init {
layout = java.awt.BorderLayout()
isOpaque = false
layout = BorderLayout()
isFocusable = true
}
override fun updateUI() {
super.updateUI()
background = UiStyle.Colors.bg()
isOpaque = true
}
override fun contains(x: Int, y: Int): Boolean {
if (!isVisible) return false
return super.contains(x, y)
}
override fun doLayout() {
super.doLayout()
components.forEach { it.doLayout() }
}
}
}
@@ -2,8 +2,12 @@ package ai.kilocode.client.migration
import ai.kilocode.client.session.SessionUiTestBase
import ai.kilocode.client.session.ui.SessionRootPanel
import ai.kilocode.client.session.ui.prompt.PromptPanel
import ai.kilocode.client.migration.ui.MigrationOverlayPanel
import ai.kilocode.client.migration.ui.MigrationWizardPanel
import ai.kilocode.rpc.dto.LegacyMigrationDetectionDto
import ai.kilocode.rpc.dto.MigrationProviderInfoDto
import java.awt.Rectangle
@Suppress("UnstableApiUsage")
class SessionUiMigrationTest : SessionUiTestBase() {
@@ -29,7 +33,19 @@ class SessionUiMigrationTest : SessionUiTestBase() {
val root = find<SessionRootPanel>(ui)
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
settle()
layout()
assertTrue("blocker should be visible", root.blocker.isVisible)
assertTrue("blocker should be opaque", root.blocker.isOpaque)
assertEquals(Rectangle(0, 0, root.width, root.height), root.blocker.bounds)
assertEquals(1, root.blocker.componentCount)
}
fun `test migration opens on selection screen with keep file checked`() {
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
settle()
val wizard = find<MigrationWizardPanel>(ui)
assertTrue(wizard.keepLegacySettingsFileSelectedForTest())
}
fun `test hidden state after visible hides blocker`() {
@@ -41,6 +57,7 @@ class SessionUiMigrationTest : SessionUiTestBase() {
fakeMigration._state.value = MigrationUiState.Hidden
settle()
assertFalse(root.blocker.isVisible)
assertEquals(0, root.blocker.componentCount)
}
fun `test two session UIs sharing one controller both react to state change`() {
@@ -64,9 +81,20 @@ class SessionUiMigrationTest : SessionUiTestBase() {
settle()
val root = find<SessionRootPanel>(ui)
assertTrue("blocker should be visible for defaultFocused test", root.blocker.isVisible)
// defaultFocusedComponent should not throw and should not be the prompt editor
val focused = ui.defaultFocusedComponent
assertNotNull(focused)
val overlay = find<MigrationOverlayPanel>(ui)
assertSame(overlay.preferredFocusComponent(), ui.defaultFocusedComponent)
assertNotSame(find<PromptPanel>(ui).defaultFocusedComponent, ui.defaultFocusedComponent)
}
fun `test migration modal covers prompt with opaque background`() {
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
settle()
layout()
val root = find<SessionRootPanel>(ui)
assertTrue(root.blocker.isVisible)
assertTrue(root.blocker.isOpaque)
assertEquals(Rectangle(0, 0, root.width, root.height), root.blocker.bounds)
}
private fun sampleDetection() = LegacyMigrationDetectionDto(
@@ -1,5 +1,6 @@
package ai.kilocode.client.session.ui
import ai.kilocode.client.ui.UiStyle
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.Dimension
@@ -26,6 +27,13 @@ class SessionRootPanelTest : BasePlatformTestCase() {
assertFalse(root.blocker.isVisible)
}
fun `test blocker is opaque and uses panel background`() {
val root = SessionRootPanel()
assertTrue(root.blocker.isOpaque)
assertEquals(UiStyle.Colors.bg(), root.blocker.background)
}
fun `test root layout fills all immediate children`() {
val root = SessionRootPanel().apply {
setSize(320, 180)
@@ -76,6 +84,29 @@ class SessionRootPanelTest : BasePlatformTestCase() {
assertFalse(root.blocker.isVisible)
}
fun `test modal content is centered inside blocker`() {
val root = SessionRootPanel().apply { setSize(200, 100) }
val child = Probe()
root.setModalContent(child)
root.doLayout()
assertTrue(root.blocker.isVisible)
assertEquals(1, root.blocker.componentCount)
assertEquals(Rectangle(60, 38, 80, 24), child.bounds)
}
fun `test clearing modal content hides and removes blocker children`() {
val root = SessionRootPanel().apply { setSize(200, 100) }
root.setModalContent(Probe())
root.doLayout()
root.setModalContent(null)
assertFalse(root.blocker.isVisible)
assertEquals(0, root.blocker.componentCount)
}
fun `test blocker contains returns false when hidden`() {
val root = SessionRootPanel().apply { setSize(200, 100) }
root.doLayout()