diff --git a/.changeset/jetbrains-migration-modal.md b/.changeset/jetbrains-migration-modal.md new file mode 100644 index 00000000000..71841b3bd1d --- /dev/null +++ b/.changeset/jetbrains-migration-modal.md @@ -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. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 8f59e910c98..c3d029451ed 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -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() } } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionRootPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionRootPanel.kt index 6ad7d29af3b..d4c0774b7e8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionRootPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionRootPanel.kt @@ -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() } + } } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/migration/SessionUiMigrationTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/migration/SessionUiMigrationTest.kt index 4f0a59b78e1..2f68c705de3 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/migration/SessionUiMigrationTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/migration/SessionUiMigrationTest.kt @@ -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(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(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(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(ui) + assertSame(overlay.preferredFocusComponent(), ui.defaultFocusedComponent) + assertNotSame(find(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(ui) + + assertTrue(root.blocker.isVisible) + assertTrue(root.blocker.isOpaque) + assertEquals(Rectangle(0, 0, root.width, root.height), root.blocker.bounds) } private fun sampleDetection() = LegacyMigrationDetectionDto( diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionRootPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionRootPanelTest.kt index d5e14e2b360..f7e69f4ca46 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionRootPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionRootPanelTest.kt @@ -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()