From 4454800d59c74ff4919ddd7714c53e1642c45585 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 30 Jun 2026 13:24:22 -0400 Subject: [PATCH 1/6] fix(jetbrains): float connection status above prompt --- .../ai/kilocode/client/session/SessionUi.kt | 18 +++++++-- .../client/session/ui/ConnectionPanel.kt | 25 +++++++++--- .../client/session/ui/prompt/PromptPanel.kt | 5 +-- .../client/session/ui/style/SessionUiStyle.kt | 20 +++------- .../client/session/SessionUiLayoutTest.kt | 39 +++++++++++++------ .../client/session/ui/ConnectionPanelTest.kt | 10 ++++- 6 files changed, 75 insertions(+), 42 deletions(-) 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 3b4acdefc47..7ae7dc9cc1a 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 @@ -48,7 +48,6 @@ import ai.kilocode.client.session.views.question.QuestionView import ai.kilocode.client.settings.KiloSettingsConfigurable import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.telemetry.Telemetry -import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.util.UiTimerSource import ai.kilocode.client.util.UiTimers import ai.kilocode.client.vfs.KiloVfsManager @@ -88,6 +87,7 @@ import java.net.URI import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.SwingUtilities import javax.swing.UIManager /** @@ -342,7 +342,6 @@ class SessionUi( scroll = SessionScroll(root, sessionContent, messageBody, blankBody) scroll.onScroll = overlay::clear - connection = ConnectionPanel(this, controller) completion = KiloPromptCompletionProvider( workspace = workspace, @@ -360,6 +359,18 @@ class SessionUi( onMentions = ::mentionParts, completion = completion, ) + connection = ConnectionPanel(this, controller) + root.addOverlay(connection) { pane, child -> + val size = child.preferredSize + val point = SwingUtilities.convertPoint(prompt.parent ?: root.content, prompt.x, prompt.y, pane) + val overlap = SessionUiStyle.View.Outline.width() + java.awt.Rectangle( + point.x, + point.y - size.height - overlap, + prompt.width, + size.height, + ) + } drop = SessionDropOverlay() root.addOverlay(drop) { pane, _ -> @@ -374,7 +385,7 @@ class SessionUi( sessionContent.add(header, BorderLayout.NORTH) sessionContent.add(scroll.component, BorderLayout.CENTER) root.content.add(sessionContent, BorderLayout.CENTER) - root.content.add(Stack.vertical().next(connection).next(prompt), BorderLayout.SOUTH) + root.content.add(prompt, BorderLayout.SOUTH) add(root, BorderLayout.CENTER) } @@ -760,6 +771,7 @@ class SessionUi( load.applyStyle(style) header.applyStyle(style) prompt.applyStyle(style) + connection.applyStyle(style) scroll.applyStyle(style) refresh() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 63d42d0cb5e..88ca5092643 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -1,10 +1,12 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.session.controller.SessionControllerEvent import ai.kilocode.client.session.controller.SessionControllerListener +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable @@ -26,7 +28,7 @@ import javax.swing.ScrollPaneConstants class ConnectionPanel( parent: Disposable, private val controller: SessionController, -) : BorderLayoutPanel(), SessionControllerListener, Disposable { +) : BorderLayoutPanel(), SessionControllerListener, Disposable, SessionEditorStyleTarget { companion object { private const val DETAILS_LINES = 10 @@ -40,7 +42,7 @@ class ConnectionPanel( } private val header = BorderLayoutPanel().apply { - border = JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), UiStyle.Gap.sm(), UiStyle.Gap.lg()) } private val left = BorderLayoutPanel().apply { @@ -77,7 +79,7 @@ class ConnectionPanel( } private val scroll = JBScrollPane(details).apply { - border = JBUI.Borders.empty(0, UiStyle.Gap.lg(), UiStyle.Gap.sm(), 0) + border = detailsBorder() // Match the banner background while retaining platform scroll behavior. isOpaque = false viewport.isOpaque = false @@ -93,8 +95,7 @@ class ConnectionPanel( Disposer.register(parent, this) // Keep the banner solid so expanded details cover transcript content beneath it. isOpaque = true - background = UiStyle.Colors.bg() - border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width(), 0, 0, 0) + applyStyle(SessionEditorStyle.current()) left.add(toggle, BorderLayout.WEST) left.add(label, BorderLayout.CENTER) header.add(left, BorderLayout.CENTER) @@ -205,6 +206,18 @@ class ConnectionPanel( // no-op } + override fun applyStyle(style: SessionEditorStyle) { + background = style.editorScheme.defaultBackground + scroll.border = detailsBorder() + revalidate() + repaint() + } + + private fun detailsBorder() = JBUI.Borders.compound( + JBUI.Borders.customLineTop(SessionUiStyle.View.Prompt.separator()), + JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), UiStyle.Gap.sm(), 0), + )!! + override fun getPreferredSize(): Dimension { val size = super.getPreferredSize() if (!scroll.isVisible) return size diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 676c265fca7..65016594979 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -58,7 +58,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.util.IconLoader import com.intellij.ui.AnimatedIcon -import com.intellij.ui.JBColor import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.xml.util.XmlStringUtil import com.intellij.util.ui.JBDimension @@ -294,7 +293,7 @@ class PromptPanel( override fun updateUI() { super.updateUI() border = JBUI.Borders.compound( - JBUI.Borders.customLineTop(separator()), + JBUI.Borders.customLineTop(SessionUiStyle.View.Prompt.separator()), JBUI.Borders.empty(), ) } @@ -791,8 +790,6 @@ class PromptPanel( return KiloBundle.message("prompt.placeholder") } - private fun separator() = JBColor.namedColor("EditorTabs.underTabsBorderColor", JBUI.CurrentTheme.EditorTabs.borderColor()) - @RequiresEdt private fun syncEditorHeight() { val before = editor.preferredSize.height diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index d6da98a2162..f6ad45966f6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -6,7 +6,6 @@ import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.Color import java.awt.Insets -import javax.swing.border.Border /** Static style tokens owned by the chat session UI. */ object SessionUiStyle { @@ -74,6 +73,11 @@ object SessionUiStyle { const val CONTROL_GAP = 4 const val SHELL_VERTICAL_PADDING = 6 const val SHELL_HORIZONTAL_PADDING = 8 + + fun separator(): Color = JBColor.namedColor( + "EditorTabs.underTabsBorderColor", + JBUI.CurrentTheme.EditorTabs.borderColor(), + ) } /** Attachment preview card geometry. */ @@ -175,17 +179,3 @@ object SessionUiStyle { val STEP: Color = JBColor.namedColor("Kilo.Session.Timeline.Step", JBColor.border()) } } - -/** Border presets for connection dock panel. */ -object Dock { - fun banner(): Border = JBUI.Borders.compound( - JBUI.Borders.customLine( - SessionUiStyle.View.Outline.color(), - SessionUiStyle.View.Outline.width(), - 0, - 0, - 0, - ), - JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()), - )!! -} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 9fa3063df02..e470948dea4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -45,16 +45,15 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertFalse(root.blocker.isVisible) } - fun `test bottom stack contains connection and prompt only`() { + fun `test prompt is docked and connection is overlaid`() { val root = find(ui) val connection = find(ui) val prompt = find(ui) - val stack = prompt.parent - assertSame(root.content, stack.parent) - assertSame(stack, connection.parent) + assertSame(root.content, prompt.parent) + assertSame(root.overlay, connection.parent) assertTrue(root.overlay.components.any { it is SessionAccountOverlay }) - assertEquals(listOf(connection, prompt), stack.components.toList()) + assertFalse(root.content.components.contains(connection)) } fun `test drop overlay is attached under root overlay layer`() { @@ -140,19 +139,35 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertSame(prompt.defaultFocusedComponent, ui.defaultFocusedComponent) } - fun `test connection panel uses stack width and sits above prompt`() { + fun `test connection panel overlays above full prompt width`() { + val root = find(ui) val connection = find(ui) val prompt = find(ui) - val stack = prompt.parent showConnection() layout() assertTrue(connection.isVisible) - assertEquals(0, connection.x) - assertEquals(stack.width, connection.width) + assertSame(root.overlay, connection.parent) + assertEquals(prompt.x, connection.x) assertEquals(prompt.width, connection.width) - assertTrue(connection.y + connection.height <= prompt.y) + assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) + } + + fun `test expanded connection panel remains anchored above prompt`() { + val connection = find(ui) + val prompt = find(ui) + + connection.onEvent(SessionControllerEvent.ConnectionChanged.ShowError( + "CLI startup failed", + "line 1\nline 2", + )) + layout() + connection.clickSummary() + layout() + + assertTrue(connection.detailsVisible()) + assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) } fun `test connection panel is unaffected by active question view`() { @@ -170,7 +185,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertTrue(find(ui).isVisible) assertSame(find(ui), find(ui).parent) assertEquals(top, connection.y) - assertTrue(connection.y + connection.height <= prompt.y) + assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) assertSame(find(ui), scrollView()) } @@ -189,7 +204,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertTrue(find(ui).isVisible) assertSame(find(ui), find(ui).parent) assertEquals(top, connection.y) - assertTrue(connection.y + connection.height <= prompt.y) + assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) assertSame(find(ui), scrollView()) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index 7507db6943f..cbb1f188631 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -3,11 +3,14 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.session.controller.SessionControllerEvent import ai.kilocode.client.session.controller.SessionControllerTestBase +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto +import com.intellij.ui.components.JBScrollPane import java.awt.Dimension +import javax.swing.border.CompoundBorder @Suppress("UnstableApiUsage") class ConnectionPanelTest : SessionControllerTestBase() { @@ -58,6 +61,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertTrue(panel.toggleExpanded()) assertTrue(panel.detailsVisible()) + assertTrue(panel.components.filterIsInstance().single().border is CompoundBorder) edt { panel.clickToggle() } @@ -157,8 +161,10 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertFalse(panel.isVisible) } - fun `test panel has top separator`() { - assertTrue(panel.hasSeparator()) + fun `test panel uses prompt background without separator`() { + assertTrue(panel.isOpaque) + assertEquals(SessionEditorStyle.current().editorScheme.defaultBackground.rgb, panel.background.rgb) + assertFalse(panel.hasSeparator()) } private fun lines(count: Int) = (1..count).joinToString("\n") { "line $it" } From 057dc97fb64afa40aa181fe2873e6f1accb73f85 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 30 Jun 2026 15:48:46 -0400 Subject: [PATCH 2/6] fix(jetbrains): show CLI recovery popup from retry --- .../client/actions/KiloActionPlaces.kt | 9 +++++ .../client/actions/ReinstallKiloAction.kt | 4 +++ .../client/actions/RestartKiloAction.kt | 4 +++ .../client/session/ui/ConnectionPanel.kt | 24 +++++++++++-- .../resources/messages/KiloBundle.properties | 2 ++ .../client/actions/KiloRecoveryActionsTest.kt | 22 ++++++++++-- .../client/session/ui/ConnectionPanelTest.kt | 36 +++++-------------- 7 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloActionPlaces.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloActionPlaces.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloActionPlaces.kt new file mode 100644 index 00000000000..75c231459a8 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloActionPlaces.kt @@ -0,0 +1,9 @@ +package ai.kilocode.client.actions + +import com.intellij.openapi.actionSystem.ActionPlaces + +internal object KiloActionPlaces { + const val CONNECTION_RETRY = "Kilo.ConnectionRetry" + + fun connectionRetryPopup() = ActionPlaces.getActionGroupPopupPlace(CONNECTION_RETRY) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt index e6d982347ac..1af6b9877b4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -15,5 +16,8 @@ class ReinstallKiloAction : AnAction(), DumbAware { override fun update(e: AnActionEvent) { e.presentation.isEnabled = true + if (e.place == KiloActionPlaces.connectionRetryPopup()) { + e.presentation.text = KiloBundle.message("action.Kilo.Reinstall.cli.text") + } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt index 93d356c78d6..1875153edd6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -15,5 +16,8 @@ class RestartKiloAction : AnAction(), DumbAware { override fun update(e: AnActionEvent) { e.presentation.isEnabled = true + if (e.place == KiloActionPlaces.connectionRetryPopup()) { + e.presentation.text = KiloBundle.message("action.Kilo.Restart.cli.text") + } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 88ca5092643..d3f280c5868 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.ui +import ai.kilocode.client.actions.KiloActionPlaces import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.session.controller.SessionControllerEvent @@ -8,8 +9,12 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle +import com.intellij.ide.DataManager import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.Disposable +import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Disposer import com.intellij.ui.components.ActionLink import com.intellij.ui.components.JBLabel @@ -31,6 +36,7 @@ class ConnectionPanel( ) : BorderLayoutPanel(), SessionControllerListener, Disposable, SessionEditorStyleTarget { companion object { + internal const val RETRY_GROUP_ID = "Kilo.CliGroup" private const val DETAILS_LINES = 10 private const val CHROME = 2 } @@ -61,7 +67,7 @@ class ConnectionPanel( } private val retry = ActionLink(KiloBundle.message("session.connection.retry")) { - controller.retryConnection() + showRecoveryPopup() }.apply { isVisible = false horizontalAlignment = JBLabel.RIGHT @@ -202,6 +208,20 @@ class ConnectionPanel( repaint() } + private fun showRecoveryPopup() { + val group = ActionManager.getInstance().getAction(RETRY_GROUP_ID) as? ActionGroup ?: return + JBPopupFactory.getInstance() + .createActionGroupPopup( + null, + group, + DataManager.getInstance().getDataContext(retry), + JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, + true, + KiloActionPlaces.connectionRetryPopup(), + ) + .showUnderneathOf(retry) + } + override fun dispose() { // no-op } @@ -264,8 +284,6 @@ class ConnectionPanel( internal fun retryFocusable() = retry.isFocusable - internal fun clickRetry() = retry.doClick() - internal fun hasSeparator() = border != null internal fun maxExpandedHeight() = diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 8ef217d0c31..c1c91b1c91f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -373,8 +373,10 @@ action.Kilo.OpenConfig.failed=Failed to open Kilo config file action.Kilo.CliGroup.text=CLI action.Kilo.CliGroup.description=Kilo CLI actions action.Kilo.Restart.text=Restart +action.Kilo.Restart.cli.text=Restart CLI action.Kilo.Restart.description=Kill and restart the CLI process action.Kilo.Reinstall.text=Reinstall +action.Kilo.Reinstall.cli.text=Reinstall CLI action.Kilo.Reinstall.description=Re-extract the CLI binary and restart action.Kilo.Session.Open.text=Open action.Kilo.Session.Open.description=Open the selected session diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index 5ab38d4ed47..5cfba0c2109 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -63,6 +63,24 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertTrue("Reinstall should force-enable recovery action", event.presentation.isEnabled) } + fun `test restart action adds cli suffix in connection retry popup`() { + val action = RestartKiloAction() + val event = event(action, place = KiloActionPlaces.connectionRetryPopup()) + + update(action, event) + + assertEquals("Restart CLI", event.presentation.text) + } + + fun `test reinstall action adds cli suffix in connection retry popup`() { + val action = ReinstallKiloAction() + val event = event(action, place = KiloActionPlaces.connectionRetryPopup()) + + update(action, event) + + assertEquals("Reinstall CLI", event.presentation.text) + } + fun `test cli group has visible menu text`() { val xml = requireNotNull(javaClass.classLoader.getResourceAsStream("kilo.jetbrains.frontend.xml")) .bufferedReader() @@ -145,10 +163,10 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertEquals(0, rpc.localConfigPathCalls) } - private fun event(action: AnAction, workspace: Workspace? = null): AnActionEvent { + private fun event(action: AnAction, workspace: Workspace? = null, place: String = ""): AnActionEvent { val presentation = Presentation().apply { copyFrom(action.templatePresentation) } presentation.isEnabled = false - return AnActionEvent.createFromDataContext("", presentation, context(workspace)) + return AnActionEvent.createFromDataContext(place, presentation, context(workspace)) } private fun update(action: AnAction, event: AnActionEvent) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index cbb1f188631..b1ccd46688f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -5,9 +5,6 @@ import ai.kilocode.client.session.controller.SessionControllerEvent import ai.kilocode.client.session.controller.SessionControllerTestBase import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle -import ai.kilocode.rpc.dto.ConfigWarningDto -import ai.kilocode.rpc.dto.KiloAppStateDto -import ai.kilocode.rpc.dto.KiloAppStatusDto import com.intellij.ui.components.JBScrollPane import java.awt.Dimension import javax.swing.border.CompoundBorder @@ -83,20 +80,19 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertEquals("Try again", panel.retryText()) } - fun `test retry click triggers app retry for app error`() { + fun `test retry popup group uses cli recovery actions`() { edt { - controller.model.app = KiloAppStateDto( - status = KiloAppStatusDto.ERROR, - error = "CLI startup failed", - ) panel.onEvent(SessionControllerEvent.ConnectionChanged.ShowError("CLI startup failed", null)) } - edt { panel.clickRetry() } - flush() + val xml = requireNotNull(javaClass.classLoader.getResourceAsStream("kilo.jetbrains.frontend.xml")) + .bufferedReader() + .use { it.readText() } - assertEquals(1, appRpc.retries) - assertEquals("Connection Retry Clicked", appRpc.telemetry.last().event) - assertEquals("ERROR", appRpc.telemetry.last().properties["appStatus"]) + assertTrue(panel.retryVisible()) + assertEquals("Kilo.CliGroup", ConnectionPanel.RETRY_GROUP_ID) + assertTrue(xml.contains("")) + assertTrue(xml.contains("")) + assertTrue(xml.contains("")) } fun `test ready warnings show collapsed banner with retry`() { @@ -126,20 +122,6 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertTrue(panel.detailsVisible()) } - fun `test retry click triggers app retry for warnings`() { - edt { - controller.model.app = KiloAppStateDto( - status = KiloAppStatusDto.READY, - warnings = listOf(ConfigWarningDto(path = ".kilo/kilo.json", message = "Invalid JSON")), - ) - panel.onEvent(SessionControllerEvent.ConnectionChanged.ShowWarning("Configuration warnings", null)) - } - edt { panel.clickRetry() } - flush() - - assertEquals(1, appRpc.retries) - } - fun `test expanded details height is capped at ten lines`() { edt { panel.onEvent(SessionControllerEvent.ConnectionChanged.ShowError("CLI startup failed", lines(30))) From d4db9c9be29102eca82668ded35db4348efb43da Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 30 Jun 2026 19:00:33 -0400 Subject: [PATCH 3/6] fix(jetbrains): refresh config action paths --- .changeset/jetbrains-config-paths.md | 5 + .../kilocode/backend/cli/KiloCliConfigPath.kt | 18 ++-- .../backend/cli/KiloCliConfigPathTest.kt | 51 +++++++++ .../client/actions/KiloSettingsAction.kt | 17 ++- .../client/actions/OpenConfigActions.kt | 16 ++- .../client/app/KiloWorkspaceService.kt | 35 +++++- .../client/actions/KiloRecoveryActionsTest.kt | 102 ++++++++++++++++++ .../client/testing/FakeWorkspaceRpcApi.kt | 4 + 8 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 .changeset/jetbrains-config-paths.md create mode 100644 packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt diff --git a/.changeset/jetbrains-config-paths.md b/.changeset/jetbrains-config-paths.md new file mode 100644 index 00000000000..b8690e0e74b --- /dev/null +++ b/.changeset/jetbrains-config-paths.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show resolved JetBrains config file paths and open the same global config directory used by the CLI. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliConfigPath.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliConfigPath.kt index 69e7cd137aa..6b305a3e800 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliConfigPath.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliConfigPath.kt @@ -1,23 +1,21 @@ package ai.kilocode.backend.cli -import com.intellij.openapi.util.SystemInfo import java.io.File internal object KiloCliConfigPath { + private const val APP = "kilo" + fun resolve(env: Map): File { env["KILO_CONFIG_DIR"]?.takeIf { it.isNotBlank() }?.let { return File(it) } - env["XDG_CONFIG_HOME"]?.takeIf { it.isNotBlank() }?.let { return File(it, "kilo") } - return File(defaultRoot(), "kilo") + env["XDG_CONFIG_HOME"]?.takeIf { it.isNotBlank() }?.let { return File(it, APP) } + return File(File(home(env), ".config"), APP) } fun legacySettingsFile(env: Map): File = File(resolve(env), "legacy-settings.json") - private fun defaultRoot(): File { - if (SystemInfo.isWindows) { - val app = System.getenv("APPDATA")?.takeIf { it.isNotBlank() } - if (app != null) return File(app) - } - if (SystemInfo.isMac) return File(System.getProperty("user.home"), "Library/Application Support") - return File(System.getProperty("user.home"), ".config") + private fun home(env: Map): String { + return env["HOME"]?.takeIf { it.isNotBlank() } + ?: env["USERPROFILE"]?.takeIf { it.isNotBlank() } + ?: System.getProperty("user.home") } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt new file mode 100644 index 00000000000..ee793cb7751 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt @@ -0,0 +1,51 @@ +package ai.kilocode.backend.cli + +import kotlin.test.Test +import kotlin.test.assertEquals +import java.io.File +import java.nio.file.Files + +class KiloCliConfigPathTest { + + @Test + fun `kilo config dir overrides XDG config home`() { + val dir = Files.createTempDirectory("kilo-config-dir").toFile() + val xdg = Files.createTempDirectory("kilo-xdg-config").toFile() + + val path = KiloCliConfigPath.resolve( + mapOf( + "KILO_CONFIG_DIR" to dir.absolutePath, + "XDG_CONFIG_HOME" to xdg.absolutePath, + ), + ) + + assertEquals(dir.absoluteFile, path.absoluteFile) + } + + @Test + fun `XDG config home resolves to kilo subdirectory`() { + val xdg = Files.createTempDirectory("kilo-xdg-config").toFile() + + val path = KiloCliConfigPath.resolve(mapOf("XDG_CONFIG_HOME" to xdg.absolutePath)) + + assertEquals(File(xdg, "kilo").absoluteFile, path.absoluteFile) + } + + @Test + fun `default config home matches CLI xdg fallback`() { + val home = Files.createTempDirectory("kilo-home").toFile() + + val path = KiloCliConfigPath.resolve(mapOf("HOME" to home.absolutePath)) + + assertEquals(File(File(home, ".config"), "kilo").absoluteFile, path.absoluteFile) + } + + @Test + fun `legacy settings file resolves under global config dir`() { + val home = Files.createTempDirectory("kilo-home").toFile() + + val path = KiloCliConfigPath.legacySettingsFile(mapOf("HOME" to home.absolutePath)) + + assertEquals(File(File(File(home, ".config"), "kilo"), "legacy-settings.json").absoluteFile, path.absoluteFile) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt index d3f12fb4089..203a0b0055e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt @@ -1,10 +1,14 @@ package ai.kilocode.client.actions +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.session.SessionManager import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionGroupUtil import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service import com.intellij.openapi.ui.popup.JBPopupFactory /** @@ -18,21 +22,32 @@ class KiloSettingsAction : AnAction() { companion object { const val GROUP_ID = "Kilo.SettingsGroup" + + internal fun popupGroup(group: ActionGroup): ActionGroup { + return ActionGroupUtil.forceRecursiveUpdateInBackground(group) + } } override fun actionPerformed(e: AnActionEvent) { val component = e.inputEvent?.component ?: return val group = ActionManager.getInstance().getAction(GROUP_ID) as? ActionGroup ?: return + val service = service() + directory(e)?.let { service.refreshLocalConfigTarget(it) } + service.refreshGlobalConfigTarget() Telemetry.send("Settings Opened", mapOf("surface" to "tool_window")) JBPopupFactory.getInstance() .createActionGroupPopup( null, - group, + popupGroup(group), e.dataContext, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true, ) .showUnderneathOf(component) } + + private fun directory(e: AnActionEvent): String? { + return e.getData(SessionManager.WORKSPACE_KEY)?.directory ?: e.project?.basePath + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt index 3a9d9cbe718..ed4e9649940 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt @@ -38,8 +38,14 @@ class OpenLocalConfigAction : ConfigAction( ) { override fun update(e: AnActionEvent) { val dir = directory(e) + val service = service() + val target = dir?.let { service.localConfig[it] } e.presentation.isEnabled = dir != null - e.presentation.text = text(dir?.let { service().localConfig[it] }) + e.presentation.text = text(target) + + if (dir != null && target == null) { + service.refreshLocalConfigTarget(dir) + } } override fun actionPerformed(e: AnActionEvent) { @@ -62,7 +68,13 @@ class OpenGlobalConfigAction : ConfigAction( description = KiloBundle.message("action.Kilo.OpenGlobalConfig.description"), ) { override fun update(e: AnActionEvent) { - e.presentation.text = text(service().globalConfig) + val service = service() + val target = service.globalConfig + e.presentation.text = text(target) + + if (target == null) { + service.refreshGlobalConfigTarget() + } } override fun actionPerformed(e: AnActionEvent) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index 14aa80eb69c..fb6dd708d9c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -10,6 +10,7 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto import ai.kilocode.rpc.dto.LoadErrorDto import ai.kilocode.rpc.dto.ModelsWorkspaceDto import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.ide.ActivityTracker import com.intellij.openapi.components.Service import ai.kilocode.log.KiloLog import com.intellij.platform.project.ProjectId @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean /** * App-level service that manages [Workspace] instances keyed by directory. @@ -46,6 +48,8 @@ class KiloWorkspaceService internal constructor( private val workspaces = ConcurrentHashMap() internal val localConfig = ConcurrentHashMap() + private val pendingLocal = ConcurrentHashMap.newKeySet() + private val pendingGlobal = AtomicBoolean(false) @Volatile internal var globalConfig: ConfigTargetDto? = null @@ -74,12 +78,15 @@ class KiloWorkspaceService internal constructor( * for the same directory share the same instance. */ fun workspace(directory: String): Workspace { - return workspaces.getOrPut(directory) { + val workspace = workspaces.getOrPut(directory) { LOG.info("Creating workspace for $directory") val state = stream { state(directory) } .stateIn(cs, SharingStarted.Eagerly, INIT) Workspace(directory, state) { reload(directory) } } + refreshLocalConfigTarget(directory) + refreshGlobalConfigTarget() + return workspace } /** @@ -181,6 +188,32 @@ class KiloWorkspaceService internal constructor( } } + fun refreshLocalConfigTarget(directory: String) { + if (!pendingLocal.add(directory)) return + + cs.launch { + try { + localConfigTarget(directory) + } finally { + pendingLocal.remove(directory) + ActivityTracker.getInstance().inc() + } + } + } + + fun refreshGlobalConfigTarget() { + if (!pendingGlobal.compareAndSet(false, true)) return + + cs.launch { + try { + globalConfigTarget() + } finally { + pendingGlobal.set(false) + ActivityTracker.getInstance().inc() + } + } + } + fun openLocalConfig(directory: String, done: (Boolean) -> Unit) { cs.launch { val ok = try { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index 5cfba0c2109..446eacf4a9c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -7,19 +7,26 @@ import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.rpc.dto.ConfigTargetDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.replaceService import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout @Suppress("UnstableApiUsage") class KiloRecoveryActionsTest : BasePlatformTestCase() { @@ -125,6 +132,41 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertEquals(0, rpc.localConfigPathCalls) } + fun `test local config action refreshes missing target in background`() { + rpc.localConfigPath = "/test/.kilo/kilo.jsonc" + rpc.localConfigDisplayPath = "/test/.kilo/kilo.jsonc" + rpc.localConfigExists = true + val action = OpenLocalConfigAction() + val event = event(action, workspace = workspace("/test")) + + update(action, event) + + assertTrue(event.presentation.isEnabled) + assertEquals("Open: local ...", event.presentation.text) + waitFor { rpc.localConfigPathCalls == 1 && service().localConfig["/test"] != null } + + val next = event(action, workspace = workspace("/test")) + update(action, next) + + assertEquals("Open: local /test/.kilo/kilo.jsonc", next.presentation.text) + assertEquals(1, rpc.localConfigPathCalls) + } + + fun `test local config action dedupes in flight refresh`() { + val gate = CompletableDeferred() + rpc.beforeLocalConfigTarget = { gate.await() } + val action = OpenLocalConfigAction() + + update(action, event(action, workspace = workspace("/test"))) + waitFor { rpc.localConfigPathCalls == 1 } + update(action, event(action, workspace = workspace("/test"))) + + assertEquals(1, rpc.localConfigPathCalls) + + gate.complete(Unit) + waitFor { service().localConfig["/test"] != null } + } + fun `test global config action says open when target exists`() { rpc.globalConfigPath = "/config/kilo.jsonc" rpc.globalConfigDisplayPath = "~/.config/kilo/kilo.jsonc" @@ -153,6 +195,40 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertEquals(0, rpc.globalConfigPathCalls) } + fun `test global config action refreshes missing target in background`() { + rpc.globalConfigPath = "/config/kilo.jsonc" + rpc.globalConfigDisplayPath = "/config/kilo.jsonc" + rpc.globalConfigExists = true + val action = OpenGlobalConfigAction() + val event = event(action) + + update(action, event) + + assertEquals("Open: global ...", event.presentation.text) + waitFor { rpc.globalConfigPathCalls == 1 && service().globalConfig != null } + + val next = event(action) + update(action, next) + + assertEquals("Open: global /config/kilo.jsonc", next.presentation.text) + assertEquals(1, rpc.globalConfigPathCalls) + } + + fun `test global config action dedupes in flight refresh`() { + val gate = CompletableDeferred() + rpc.beforeGlobalConfigTarget = { gate.await() } + val action = OpenGlobalConfigAction() + + update(action, event(action)) + waitFor { rpc.globalConfigPathCalls == 1 } + update(action, event(action)) + + assertEquals(1, rpc.globalConfigPathCalls) + + gate.complete(Unit) + waitFor { service().globalConfig != null } + } + fun `test local config action disables without directory`() { val action = OpenLocalConfigAction() val event = event(action) @@ -163,6 +239,22 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertEquals(0, rpc.localConfigPathCalls) } + fun `test settings popup group updates recursively in background`() { + val group = DefaultActionGroup() + val wrapped = KiloSettingsAction.popupGroup(group) + + assertEquals(ActionUpdateThread.BGT, wrapped.actionUpdateThread) + } + + fun `test workspace creation prewarms config targets`() { + service().workspace("/test") + + waitFor { rpc.localConfigPathCalls == 1 && rpc.globalConfigPathCalls == 1 } + + assertEquals(1, rpc.localConfigPathCalls) + assertEquals(1, rpc.globalConfigPathCalls) + } + private fun event(action: AnAction, workspace: Workspace? = null, place: String = ""): AnActionEvent { val presentation = Presentation().apply { copyFrom(action.templatePresentation) } presentation.isEnabled = false @@ -175,6 +267,16 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { }.get() } + private fun waitFor(done: () -> Boolean) = runBlocking { + withTimeout(5_000) { + while (!done()) { + delay(25) + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + } + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + private fun service(): KiloWorkspaceService = ApplicationManager.getApplication().getService(KiloWorkspaceService::class.java) private fun cacheGlobal(target: ConfigTargetDto) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt index 0bb64d19a69..3de5b5a639a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt @@ -41,6 +41,8 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { var globalConfigDisplayPath = globalConfigPath var localConfigExists = true var globalConfigExists = true + var beforeLocalConfigTarget: (suspend () -> Unit)? = null + var beforeGlobalConfigTarget: (suspend () -> Unit)? = null val fileCalls = CopyOnWriteArrayList>() val searchQueries = CopyOnWriteArrayList() val opened = CopyOnWriteArrayList() @@ -98,12 +100,14 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { override suspend fun localConfigTarget(directory: String): ConfigTargetDto { assertNotEdt("localConfigTarget") localConfigPathCalls += 1 + beforeLocalConfigTarget?.invoke() return ConfigTargetDto(localConfigPath, localConfigDisplayPath, localConfigExists) } override suspend fun globalConfigTarget(): ConfigTargetDto { assertNotEdt("globalConfigTarget") globalConfigPathCalls += 1 + beforeGlobalConfigTarget?.invoke() return ConfigTargetDto(globalConfigPath, globalConfigDisplayPath, globalConfigExists) } From 02c1177d6e6e8c23d6449318b262c03b16907573 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 30 Jun 2026 20:14:52 -0400 Subject: [PATCH 4/6] chore(jetbrains): address config action review --- packages/kilo-jetbrains/AGENTS.md | 1 - .../backend/cli/KiloCliConfigPathTest.kt | 29 +++++++++++++++++++ .../client/actions/ActionEventWorkspace.kt | 8 +++++ .../client/actions/KiloSettingsAction.kt | 12 ++++---- .../client/actions/OpenConfigActions.kt | 9 ++---- .../client/app/KiloWorkspaceService.kt | 1 + .../client/session/ui/ConnectionPanel.kt | 4 +-- .../messages/KiloBundle_ar.properties | 2 ++ .../messages/KiloBundle_bs.properties | 2 ++ .../messages/KiloBundle_da.properties | 2 ++ .../messages/KiloBundle_de.properties | 2 ++ .../messages/KiloBundle_es.properties | 2 ++ .../messages/KiloBundle_fr.properties | 2 ++ .../messages/KiloBundle_ja.properties | 2 ++ .../messages/KiloBundle_ko.properties | 2 ++ .../messages/KiloBundle_nl.properties | 2 ++ .../messages/KiloBundle_no.properties | 2 ++ .../messages/KiloBundle_pl.properties | 2 ++ .../messages/KiloBundle_pt_BR.properties | 2 ++ .../messages/KiloBundle_ru.properties | 2 ++ .../messages/KiloBundle_th.properties | 2 ++ .../messages/KiloBundle_tr.properties | 2 ++ .../messages/KiloBundle_uk.properties | 2 ++ .../messages/KiloBundle_zh_CN.properties | 2 ++ .../messages/KiloBundle_zh_TW.properties | 2 ++ .../client/actions/KiloRecoveryActionsTest.kt | 8 +++++ .../client/session/ui/ConnectionPanelTest.kt | 2 +- 27 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ActionEventWorkspace.kt diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index 52acdbcfdc7..e50cc2e3087 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -240,7 +240,6 @@ Before introducing any new reusable color, spacing value, border, size, font, or - `SessionUiStyle.View` — card sizing, card borders, surfaces, hover colors, and nested objects for `Prompt`, `Reasoning`, `Message`, and `Tool`. - `SessionUiStyle.RecentSessions` — recent sessions list limits. - `SessionUiStyle.Timeline` — activity-indicator colors for the session header timeline. -- `Dock` — border presets for question, permission, and connection dock panels. Rules: - Generic layout constants (gaps, generic colors, reusable helpers) → `UiStyle`. diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt index ee793cb7751..bf0b4359e93 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliConfigPathTest.kt @@ -40,6 +40,35 @@ class KiloCliConfigPathTest { assertEquals(File(File(home, ".config"), "kilo").absoluteFile, path.absoluteFile) } + @Test + fun `USERPROFILE backs up HOME for default config home`() { + val home = Files.createTempDirectory("kilo-userprofile").toFile() + + val path = KiloCliConfigPath.resolve( + mapOf( + "HOME" to "", + "USERPROFILE" to home.absolutePath, + ), + ) + + assertEquals(File(File(home, ".config"), "kilo").absoluteFile, path.absoluteFile) + } + + @Test + fun `blank config env values are ignored`() { + val home = Files.createTempDirectory("kilo-home").toFile() + + val path = KiloCliConfigPath.resolve( + mapOf( + "KILO_CONFIG_DIR" to " ", + "XDG_CONFIG_HOME" to "", + "HOME" to home.absolutePath, + ), + ) + + assertEquals(File(File(home, ".config"), "kilo").absoluteFile, path.absoluteFile) + } + @Test fun `legacy settings file resolves under global config dir`() { val home = Files.createTempDirectory("kilo-home").toFile() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ActionEventWorkspace.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ActionEventWorkspace.kt new file mode 100644 index 00000000000..34f4bea552e --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ActionEventWorkspace.kt @@ -0,0 +1,8 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.session.SessionManager +import com.intellij.openapi.actionSystem.AnActionEvent + +internal fun AnActionEvent.workspaceDirectory(): String? { + return getData(SessionManager.WORKSPACE_KEY)?.directory ?: project?.basePath +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt index 203a0b0055e..02adde32df8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt @@ -1,7 +1,6 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloWorkspaceService -import ai.kilocode.client.session.SessionManager import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.ActionGroupUtil @@ -26,14 +25,18 @@ class KiloSettingsAction : AnAction() { internal fun popupGroup(group: ActionGroup): ActionGroup { return ActionGroupUtil.forceRecursiveUpdateInBackground(group) } + + internal fun refreshConfigTargets(e: AnActionEvent, service: KiloWorkspaceService) { + e.workspaceDirectory()?.let { service.refreshLocalConfigTarget(it) } + service.refreshGlobalConfigTarget() + } } override fun actionPerformed(e: AnActionEvent) { val component = e.inputEvent?.component ?: return val group = ActionManager.getInstance().getAction(GROUP_ID) as? ActionGroup ?: return val service = service() - directory(e)?.let { service.refreshLocalConfigTarget(it) } - service.refreshGlobalConfigTarget() + refreshConfigTargets(e, service) Telemetry.send("Settings Opened", mapOf("surface" to "tool_window")) JBPopupFactory.getInstance() @@ -47,7 +50,4 @@ class KiloSettingsAction : AnAction() { .showUnderneathOf(component) } - private fun directory(e: AnActionEvent): String? { - return e.getData(SessionManager.WORKSPACE_KEY)?.directory ?: e.project?.basePath - } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt index ed4e9649940..01054601b01 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.actions import ai.kilocode.client.KiloNotifications import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.SessionManager import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.rpc.dto.ConfigTargetDto import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -37,7 +36,7 @@ class OpenLocalConfigAction : ConfigAction( description = KiloBundle.message("action.Kilo.OpenLocalConfig.description"), ) { override fun update(e: AnActionEvent) { - val dir = directory(e) + val dir = e.workspaceDirectory() val service = service() val target = dir?.let { service.localConfig[it] } e.presentation.isEnabled = dir != null @@ -49,16 +48,12 @@ class OpenLocalConfigAction : ConfigAction( } override fun actionPerformed(e: AnActionEvent) { - val dir = directory(e) ?: return + val dir = e.workspaceDirectory() ?: return Telemetry.send("Config Opened", mapOf("surface" to "tool_window", "scope" to "local")) service().openLocalConfig(dir) { ok -> if (!ok) failed() } } - - private fun directory(e: AnActionEvent): String? { - return e.getData(SessionManager.WORKSPACE_KEY)?.directory ?: e.project?.basePath - } } class OpenGlobalConfigAction : ConfigAction( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index fb6dd708d9c..4e828ff7413 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -84,6 +84,7 @@ class KiloWorkspaceService internal constructor( .stateIn(cs, SharingStarted.Eagerly, INIT) Workspace(directory, state) { reload(directory) } } + // Refresh on every workspace access so config actions reflect file system changes. refreshLocalConfigTarget(directory) refreshGlobalConfigTarget() return workspace diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index d3f280c5868..68af053d556 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -36,7 +36,7 @@ class ConnectionPanel( ) : BorderLayoutPanel(), SessionControllerListener, Disposable, SessionEditorStyleTarget { companion object { - internal const val RETRY_GROUP_ID = "Kilo.CliGroup" + internal const val CLI_GROUP_ID = "Kilo.CliGroup" private const val DETAILS_LINES = 10 private const val CHROME = 2 } @@ -209,7 +209,7 @@ class ConnectionPanel( } private fun showRecoveryPopup() { - val group = ActionManager.getInstance().getAction(RETRY_GROUP_ID) as? ActionGroup ?: return + val group = ActionManager.getInstance().getAction(CLI_GROUP_ID) as? ActionGroup ?: return JBPopupFactory.getInstance() .createActionGroupPopup( null, diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index 0890f64c2cf..eacde58d1d2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=إيقاف جلسة Kilo الحالية action.Kilo.SettingsGroup.text=الإعدادات action.Kilo.SettingsGroup.description=إعدادات Kilo Code action.Kilo.Restart.text=إعادة تشغيل Kilo +action.Kilo.Restart.cli.text=إعادة تشغيل CLI action.Kilo.Restart.description=إنهاء عملية CLI وإعادة تشغيلها action.Kilo.Reinstall.text=إعادة تثبيت Kilo +action.Kilo.Reinstall.cli.text=إعادة تثبيت CLI action.Kilo.Reinstall.description=استخراج CLI مجدداً وإعادة التشغيل action.Kilo.Session.Open.text=فتح action.Kilo.Session.Open.description=فتح الجلسة المحددة diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index f26bf14c4f9..caa328e2586 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Zaustavi trenutnu Kilo sesiju action.Kilo.SettingsGroup.text=Postavke action.Kilo.SettingsGroup.description=Postavke Kilo Code action.Kilo.Restart.text=Ponovo pokrenite Kilo +action.Kilo.Restart.cli.text=Ponovo pokrenite CLI action.Kilo.Restart.description=Ugasite i ponovo pokrenite CLI proces action.Kilo.Reinstall.text=Ponovo instalirajte Kilo +action.Kilo.Reinstall.cli.text=Ponovo instalirajte CLI action.Kilo.Reinstall.description=Ponovo raspakirajte CLI binarnu datoteku i ponovo pokrenite action.Kilo.Session.Open.text=Otvori action.Kilo.Session.Open.description=Otvorite odabranu sesiju diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 001b10bad85..841338f6939 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Stop den aktuelle Kilo-session action.Kilo.SettingsGroup.text=Indstillinger action.Kilo.SettingsGroup.description=Kilo Code-indstillinger action.Kilo.Restart.text=Genstart Kilo +action.Kilo.Restart.cli.text=Genstart CLI action.Kilo.Restart.description=Afslut og genstart CLI-processen action.Kilo.Reinstall.text=Geninstaller Kilo +action.Kilo.Reinstall.cli.text=Geninstaller CLI action.Kilo.Reinstall.description=Udpak CLI-binærfilen igen og genstart action.Kilo.Session.Open.text=Åbn action.Kilo.Session.Open.description=Åbn den valgte session diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 2cab50026f4..e0487bf86cc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Aktuelle Kilo-Sitzung stoppen action.Kilo.SettingsGroup.text=Einstellungen action.Kilo.SettingsGroup.description=Kilo Code Einstellungen action.Kilo.Restart.text=Kilo neu starten +action.Kilo.Restart.cli.text=CLI neu starten action.Kilo.Restart.description=CLI-Prozess beenden und neu starten action.Kilo.Reinstall.text=Kilo neu installieren +action.Kilo.Reinstall.cli.text=CLI neu installieren action.Kilo.Reinstall.description=CLI-Binärdatei neu extrahieren und neu starten action.Kilo.Session.Open.text=Öffnen action.Kilo.Session.Open.description=Ausgewählte Sitzung öffnen diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 2eb7dbe49df..b991a266db7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Detener la sesión actual de Kilo action.Kilo.SettingsGroup.text=Configuración action.Kilo.SettingsGroup.description=Configuración de Kilo Code action.Kilo.Restart.text=Reiniciar Kilo +action.Kilo.Restart.cli.text=Reiniciar CLI action.Kilo.Restart.description=Terminar y reiniciar el proceso CLI action.Kilo.Reinstall.text=Reinstalar Kilo +action.Kilo.Reinstall.cli.text=Reinstalar CLI action.Kilo.Reinstall.description=Reextraer el binario CLI y reiniciar action.Kilo.Session.Open.text=Abrir action.Kilo.Session.Open.description=Abrir la sesión seleccionada diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index 2f437d5df60..4d1e5614558 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Arrêter la session Kilo actuelle action.Kilo.SettingsGroup.text=Paramètres action.Kilo.SettingsGroup.description=Paramètres de Kilo Code action.Kilo.Restart.text=Redémarrer Kilo +action.Kilo.Restart.cli.text=Redémarrer CLI action.Kilo.Restart.description=Tuer et redémarrer le processus CLI action.Kilo.Reinstall.text=Réinstaller Kilo +action.Kilo.Reinstall.cli.text=Réinstaller CLI action.Kilo.Reinstall.description=Réextraire le binaire CLI et redémarrer action.Kilo.Session.Open.text=Ouvrir action.Kilo.Session.Open.description=Ouvrir la session sélectionnée diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 552523ec0f3..851f89d4043 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=現在のKiloセッションを停止 action.Kilo.SettingsGroup.text=設定 action.Kilo.SettingsGroup.description=Kilo Codeの設定 action.Kilo.Restart.text=Kiloを再起動 +action.Kilo.Restart.cli.text=CLIを再起動 action.Kilo.Restart.description=CLIプロセスを終了して再起動 action.Kilo.Reinstall.text=Kiloを再インストール +action.Kilo.Reinstall.cli.text=CLIを再インストール action.Kilo.Reinstall.description=CLIバイナリを再展開して再起動 action.Kilo.Session.Open.text=開く action.Kilo.Session.Open.description=選択したセッションを開く diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 4c22c1e52b8..75d989c6a39 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=현재 Kilo 세션 정지 action.Kilo.SettingsGroup.text=설정 action.Kilo.SettingsGroup.description=Kilo Code 설정 action.Kilo.Restart.text=Kilo 재시작 +action.Kilo.Restart.cli.text=CLI 재시작 action.Kilo.Restart.description=CLI 프로세스 종료 후 재시작 action.Kilo.Reinstall.text=Kilo 재설치 +action.Kilo.Reinstall.cli.text=CLI 재설치 action.Kilo.Reinstall.description=CLI 바이너리 재추출 후 재시작 action.Kilo.Session.Open.text=열기 action.Kilo.Session.Open.description=선택한 세션 열기 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index e130d8e5d48..9b2e0dbbc9a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Huidige Kilo-sessie stoppen action.Kilo.SettingsGroup.text=Instellingen action.Kilo.SettingsGroup.description=Kilo Code-instellingen action.Kilo.Restart.text=Kilo herstarten +action.Kilo.Restart.cli.text=CLI herstarten action.Kilo.Restart.description=CLI-proces beëindigen en herstarten action.Kilo.Reinstall.text=Kilo herinstalleren +action.Kilo.Reinstall.cli.text=CLI herinstalleren action.Kilo.Reinstall.description=CLI-binair opnieuw uitpakken en herstarten action.Kilo.Session.Open.text=Openen action.Kilo.Session.Open.description=Geselecteerde sessie openen diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index e2185e84542..0b58eb9f61d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=Stopp gjeldende Kilo-økt action.Kilo.SettingsGroup.text=Innstillinger action.Kilo.SettingsGroup.description=Kilo Code-innstillinger action.Kilo.Restart.text=Start Kilo på nytt +action.Kilo.Restart.cli.text=Start CLI på nytt action.Kilo.Restart.description=Avslutt og start CLI-prosessen på nytt action.Kilo.Reinstall.text=Installer Kilo på nytt +action.Kilo.Reinstall.cli.text=Installer CLI på nytt action.Kilo.Reinstall.description=Pakk ut CLI-binærfilen på nytt og start på nytt action.Kilo.Session.Open.text=Åpne action.Kilo.Session.Open.description=Åpne valgt økt diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index eff0dfc7e4c..d44bf46123b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=Zatrzymaj bieżącą sesję Kilo action.Kilo.SettingsGroup.text=Ustawienia action.Kilo.SettingsGroup.description=Ustawienia Kilo Code action.Kilo.Restart.text=Uruchom ponownie Kilo +action.Kilo.Restart.cli.text=Uruchom ponownie CLI action.Kilo.Restart.description=Zakończ i uruchom ponownie proces CLI action.Kilo.Reinstall.text=Zainstaluj ponownie Kilo +action.Kilo.Reinstall.cli.text=Zainstaluj ponownie CLI action.Kilo.Reinstall.description=Ponownie wyodrębnij plik binarny CLI i uruchom ponownie action.Kilo.Session.Open.text=Otwórz action.Kilo.Session.Open.description=Otwórz wybraną sesję diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index efdff77ef96..76a11c7a18b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=Parar a sessão atual do Kilo action.Kilo.SettingsGroup.text=Configurações action.Kilo.SettingsGroup.description=Configurações do Kilo Code action.Kilo.Restart.text=Reiniciar Kilo +action.Kilo.Restart.cli.text=Reiniciar CLI action.Kilo.Restart.description=Encerrar e reiniciar o processo CLI action.Kilo.Reinstall.text=Reinstalar Kilo +action.Kilo.Reinstall.cli.text=Reinstalar CLI action.Kilo.Reinstall.description=Reextrair o binário CLI e reiniciar action.Kilo.Session.Open.text=Abrir action.Kilo.Session.Open.description=Abrir a sessão selecionada diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index 0eeee04d8c9..5698007f3a3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=Остановить текущую сесс action.Kilo.SettingsGroup.text=Настройки action.Kilo.SettingsGroup.description=Настройки Kilo Code action.Kilo.Restart.text=Перезапустить Kilo +action.Kilo.Restart.cli.text=Перезапустить CLI action.Kilo.Restart.description=Завершить и перезапустить CLI-процесс action.Kilo.Reinstall.text=Переустановить Kilo +action.Kilo.Reinstall.cli.text=Переустановить CLI action.Kilo.Reinstall.description=Переизвлечь CLI-бинарник и перезапустить action.Kilo.Session.Open.text=Открыть action.Kilo.Session.Open.description=Открыть выбранную сессию diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index a5184f76a91..b041588c377 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=หยุดเซสชัน Kilo ปั action.Kilo.SettingsGroup.text=การตั้งค่า action.Kilo.SettingsGroup.description=การตั้งค่า Kilo Code action.Kilo.Restart.text=รีสตาร์ต Kilo +action.Kilo.Restart.cli.text=รีสตาร์ต CLI action.Kilo.Restart.description=สิ้นสุดและรีสตาร์ตกระบวนการ CLI action.Kilo.Reinstall.text=ติดตั้ง Kilo ใหม่ +action.Kilo.Reinstall.cli.text=ติดตั้ง CLI ใหม่ action.Kilo.Reinstall.description=แตกไฟล์ไบนารี CLI อีกครั้งและรีสตาร์ต action.Kilo.Session.Open.text=เปิด action.Kilo.Session.Open.description=เปิดเซสชันที่เลือก diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index f9efbf5b189..891ad2e5ca6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -139,8 +139,10 @@ action.Kilo.StopSession.description=Geçerli Kilo oturumunu durdur action.Kilo.SettingsGroup.text=Ayarlar action.Kilo.SettingsGroup.description=Kilo Code ayarları action.Kilo.Restart.text=Kilo’yu yeniden başlat +action.Kilo.Restart.cli.text=CLI’yi yeniden başlat action.Kilo.Restart.description=CLI sürecini sonlandır ve yeniden başlat action.Kilo.Reinstall.text=Kilo’yu yeniden yükle +action.Kilo.Reinstall.cli.text=CLI’yi yeniden yükle action.Kilo.Reinstall.description=CLI ikili dosyasını yeniden çıkar ve yeniden başlat action.Kilo.Session.Open.text=Aç action.Kilo.Session.Open.description=Seçilen oturumu aç diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 6d3efd7480c..8ba5a608a05 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=Зупинити поточну сесію K action.Kilo.SettingsGroup.text=Налаштування action.Kilo.SettingsGroup.description=Налаштування Kilo Code action.Kilo.Restart.text=Перезапустити Kilo +action.Kilo.Restart.cli.text=Перезапустити CLI action.Kilo.Restart.description=Завершити і перезапустити CLI-процес action.Kilo.Reinstall.text=Перевстановити Kilo +action.Kilo.Reinstall.cli.text=Перевстановити CLI action.Kilo.Reinstall.description=Перерозпакувати CLI-бінарний файл і перезапустити action.Kilo.Session.Open.text=Відкрити action.Kilo.Session.Open.description=Відкрити вибрану сесію diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index eea4f6d35a0..c3524f5dc18 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=停止当前 Kilo 会话 action.Kilo.SettingsGroup.text=设置 action.Kilo.SettingsGroup.description=Kilo Code 设置 action.Kilo.Restart.text=重启 Kilo +action.Kilo.Restart.cli.text=重启 CLI action.Kilo.Restart.description=终止并重启 CLI 进程 action.Kilo.Reinstall.text=重新安装 Kilo +action.Kilo.Reinstall.cli.text=重新安装 CLI action.Kilo.Reinstall.description=重新解压 CLI 二进制文件并重启 action.Kilo.Session.Open.text=打开 action.Kilo.Session.Open.description=打开所选会话 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index 4c771f8861f..4e41ed4ec2f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -134,8 +134,10 @@ action.Kilo.StopSession.description=停止目前的 Kilo 工作階段 action.Kilo.SettingsGroup.text=設定 action.Kilo.SettingsGroup.description=Kilo Code 設定 action.Kilo.Restart.text=重啟 Kilo +action.Kilo.Restart.cli.text=重啟 CLI action.Kilo.Restart.description=終止並重啟 CLI 程序 action.Kilo.Reinstall.text=重新安裝 Kilo +action.Kilo.Reinstall.cli.text=重新安裝 CLI action.Kilo.Reinstall.description=重新解壓縮 CLI 二進位檔案並重啟 action.Kilo.Session.Open.text=開啟 action.Kilo.Session.Open.description=開啟所選工作階段 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index 446eacf4a9c..49bcfe3bde7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -246,6 +246,14 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertEquals(ActionUpdateThread.BGT, wrapped.actionUpdateThread) } + fun `test settings action prewarms config targets`() { + val action = KiloSettingsAction() + + KiloSettingsAction.refreshConfigTargets(event(action, workspace = workspace("/test")), service()) + + waitFor { rpc.localConfigPathCalls == 1 && rpc.globalConfigPathCalls == 1 } + } + fun `test workspace creation prewarms config targets`() { service().workspace("/test") diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index b1ccd46688f..deb5a8d94e8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -89,7 +89,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { .use { it.readText() } assertTrue(panel.retryVisible()) - assertEquals("Kilo.CliGroup", ConnectionPanel.RETRY_GROUP_ID) + assertEquals("Kilo.CliGroup", ConnectionPanel.CLI_GROUP_ID) assertTrue(xml.contains("")) assertTrue(xml.contains("")) assertTrue(xml.contains("")) From d5f9e53e73e66498a967c91784589c63ec79cd50 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 1 Jul 2026 10:22:59 -0400 Subject: [PATCH 5/6] fix(jetbrains): address recovery review feedback --- .changeset/jetbrains-config-paths.md | 2 +- .../client/actions/KiloSettingsAction.kt | 9 ++- .../client/app/KiloWorkspaceService.kt | 13 ++-- .../client/session/ui/ConnectionPanel.kt | 21 +++++- .../client/actions/KiloRecoveryActionsTest.kt | 72 ++++++++++++------- .../client/session/ui/ConnectionPanelTest.kt | 3 +- 6 files changed, 83 insertions(+), 37 deletions(-) diff --git a/.changeset/jetbrains-config-paths.md b/.changeset/jetbrains-config-paths.md index b8690e0e74b..80bdc99513c 100644 --- a/.changeset/jetbrains-config-paths.md +++ b/.changeset/jetbrains-config-paths.md @@ -2,4 +2,4 @@ "@kilocode/kilo-jetbrains": patch --- -Show resolved JetBrains config file paths and open the same global config directory used by the CLI. +Show resolved JetBrains config file paths, float connection status above the prompt, and offer retry, restart, and reinstall recovery actions from connection errors. JetBrains now opens the same global config directory used by the CLI; macOS and Windows users who previously created global config from JetBrains may need to move files from the old platform-specific location to `~/.config/kilo`. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt index 02adde32df8..dd42fee0701 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service import com.intellij.openapi.ui.popup.JBPopupFactory +import kotlinx.coroutines.Job /** * Gear icon action placed in the Kilo tool window title bar. @@ -26,9 +27,11 @@ class KiloSettingsAction : AnAction() { return ActionGroupUtil.forceRecursiveUpdateInBackground(group) } - internal fun refreshConfigTargets(e: AnActionEvent, service: KiloWorkspaceService) { - e.workspaceDirectory()?.let { service.refreshLocalConfigTarget(it) } - service.refreshGlobalConfigTarget() + internal fun refreshConfigTargets(e: AnActionEvent, service: KiloWorkspaceService): List { + return listOfNotNull( + e.workspaceDirectory()?.let { service.refreshLocalConfigTarget(it) }, + service.refreshGlobalConfigTarget(), + ) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index 4e828ff7413..b19cc6a76e4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -17,6 +17,7 @@ import com.intellij.platform.project.ProjectId import fleet.rpc.client.durable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flow @@ -189,10 +190,10 @@ class KiloWorkspaceService internal constructor( } } - fun refreshLocalConfigTarget(directory: String) { - if (!pendingLocal.add(directory)) return + fun refreshLocalConfigTarget(directory: String): Job? { + if (!pendingLocal.add(directory)) return null - cs.launch { + return cs.launch { try { localConfigTarget(directory) } finally { @@ -202,10 +203,10 @@ class KiloWorkspaceService internal constructor( } } - fun refreshGlobalConfigTarget() { - if (!pendingGlobal.compareAndSet(false, true)) return + fun refreshGlobalConfigTarget(): Job? { + if (!pendingGlobal.compareAndSet(false, true)) return null - cs.launch { + return cs.launch { try { globalConfigTarget() } finally { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 68af053d556..8101e746534 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -13,7 +13,10 @@ import com.intellij.ide.DataManager import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.Disposable +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Disposer import com.intellij.ui.components.ActionLink @@ -209,11 +212,10 @@ class ConnectionPanel( } private fun showRecoveryPopup() { - val group = ActionManager.getInstance().getAction(CLI_GROUP_ID) as? ActionGroup ?: return JBPopupFactory.getInstance() .createActionGroupPopup( null, - group, + recoveryGroup(), DataManager.getInstance().getDataContext(retry), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true, @@ -222,6 +224,19 @@ class ConnectionPanel( .showUnderneathOf(retry) } + private fun recoveryGroup(): ActionGroup { + val group = DefaultActionGroup() + group.add(object : DumbAwareAction(KiloBundle.message("session.connection.retry")) { + override fun actionPerformed(e: AnActionEvent) { + controller.retryConnection() + } + }) + group.addSeparator() + ActionManager.getInstance().getAction("Kilo.Restart")?.let { group.add(it) } + ActionManager.getInstance().getAction("Kilo.Reinstall")?.let { group.add(it) } + return group + } + override fun dispose() { // no-op } @@ -284,6 +299,8 @@ class ConnectionPanel( internal fun retryFocusable() = retry.isFocusable + internal fun recoveryActionTexts() = recoveryGroup().getChildren(null).mapNotNull { it.templatePresentation.text } + internal fun hasSeparator() = border != null internal fun maxExpandedHeight() = diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index 49bcfe3bde7..14b0d61e4e6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -18,12 +18,10 @@ import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.replaceService import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.intellij.util.ui.UIUtil import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout @@ -136,6 +134,12 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { rpc.localConfigPath = "/test/.kilo/kilo.jsonc" rpc.localConfigDisplayPath = "/test/.kilo/kilo.jsonc" rpc.localConfigExists = true + val call = CompletableDeferred() + val gate = CompletableDeferred() + rpc.beforeLocalConfigTarget = { + call.complete(Unit) + gate.await() + } val action = OpenLocalConfigAction() val event = event(action, workspace = workspace("/test")) @@ -143,28 +147,34 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { assertTrue(event.presentation.isEnabled) assertEquals("Open: local ...", event.presentation.text) - waitFor { rpc.localConfigPathCalls == 1 && service().localConfig["/test"] != null } + await(call) + assertEquals(1, rpc.localConfigPathCalls) + + gate.complete(Unit) + service().localConfig["/test"] = ConfigTargetDto("/test/.kilo/kilo.jsonc", "/test/.kilo/kilo.jsonc", true) val next = event(action, workspace = workspace("/test")) update(action, next) assertEquals("Open: local /test/.kilo/kilo.jsonc", next.presentation.text) - assertEquals(1, rpc.localConfigPathCalls) } fun `test local config action dedupes in flight refresh`() { val gate = CompletableDeferred() - rpc.beforeLocalConfigTarget = { gate.await() } + val call = CompletableDeferred() val action = OpenLocalConfigAction() + rpc.beforeLocalConfigTarget = { + call.complete(Unit) + gate.await() + } update(action, event(action, workspace = workspace("/test"))) - waitFor { rpc.localConfigPathCalls == 1 } + await(call) update(action, event(action, workspace = workspace("/test"))) assertEquals(1, rpc.localConfigPathCalls) gate.complete(Unit) - waitFor { service().localConfig["/test"] != null } } fun `test global config action says open when target exists`() { @@ -199,34 +209,46 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { rpc.globalConfigPath = "/config/kilo.jsonc" rpc.globalConfigDisplayPath = "/config/kilo.jsonc" rpc.globalConfigExists = true + val call = CompletableDeferred() + val gate = CompletableDeferred() + rpc.beforeGlobalConfigTarget = { + call.complete(Unit) + gate.await() + } val action = OpenGlobalConfigAction() val event = event(action) update(action, event) assertEquals("Open: global ...", event.presentation.text) - waitFor { rpc.globalConfigPathCalls == 1 && service().globalConfig != null } + await(call) + assertEquals(1, rpc.globalConfigPathCalls) + + gate.complete(Unit) + cacheGlobal(ConfigTargetDto("/config/kilo.jsonc", "/config/kilo.jsonc", true)) val next = event(action) update(action, next) assertEquals("Open: global /config/kilo.jsonc", next.presentation.text) - assertEquals(1, rpc.globalConfigPathCalls) } fun `test global config action dedupes in flight refresh`() { val gate = CompletableDeferred() - rpc.beforeGlobalConfigTarget = { gate.await() } + val call = CompletableDeferred() + rpc.beforeGlobalConfigTarget = { + call.complete(Unit) + gate.await() + } val action = OpenGlobalConfigAction() update(action, event(action)) - waitFor { rpc.globalConfigPathCalls == 1 } + await(call) update(action, event(action)) assertEquals(1, rpc.globalConfigPathCalls) gate.complete(Unit) - waitFor { service().globalConfig != null } } fun `test local config action disables without directory`() { @@ -249,16 +271,24 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { fun `test settings action prewarms config targets`() { val action = KiloSettingsAction() - KiloSettingsAction.refreshConfigTargets(event(action, workspace = workspace("/test")), service()) + runBlocking { + KiloSettingsAction.refreshConfigTargets(event(action, workspace = workspace("/test")), service()).forEach { it.join() } + } - waitFor { rpc.localConfigPathCalls == 1 && rpc.globalConfigPathCalls == 1 } + assertEquals(1, rpc.localConfigPathCalls) + assertEquals(1, rpc.globalConfigPathCalls) } fun `test workspace creation prewarms config targets`() { - service().workspace("/test") + val local = CompletableDeferred() + val global = CompletableDeferred() + rpc.beforeLocalConfigTarget = { local.complete(Unit) } + rpc.beforeGlobalConfigTarget = { global.complete(Unit) } - waitFor { rpc.localConfigPathCalls == 1 && rpc.globalConfigPathCalls == 1 } + service().workspace("/test") + await(local) + await(global) assertEquals(1, rpc.localConfigPathCalls) assertEquals(1, rpc.globalConfigPathCalls) } @@ -275,14 +305,8 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { }.get() } - private fun waitFor(done: () -> Boolean) = runBlocking { - withTimeout(5_000) { - while (!done()) { - delay(25) - ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } - } - } - ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + private fun await(signal: CompletableDeferred) = runBlocking { + withTimeout(5_000) { signal.await() } } private fun service(): KiloWorkspaceService = ApplicationManager.getApplication().getService(KiloWorkspaceService::class.java) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index deb5a8d94e8..a08002dfb47 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -80,7 +80,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertEquals("Try again", panel.retryText()) } - fun `test retry popup group uses cli recovery actions`() { + fun `test retry popup group keeps lightweight retry and cli recovery actions`() { edt { panel.onEvent(SessionControllerEvent.ConnectionChanged.ShowError("CLI startup failed", null)) } @@ -89,6 +89,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { .use { it.readText() } assertTrue(panel.retryVisible()) + assertTrue(panel.recoveryActionTexts().contains("Try again")) assertEquals("Kilo.CliGroup", ConnectionPanel.CLI_GROUP_ID) assertTrue(xml.contains("")) assertTrue(xml.contains("")) From 2a75b6cbe180ea56d67ec2dd59d2e5555d00adf4 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 1 Jul 2026 10:54:54 -0400 Subject: [PATCH 6/6] fix(jetbrains): remove test-only recovery accessor --- packages/kilo-jetbrains/AGENTS.md | 1 + .../kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt | 2 -- .../ai/kilocode/client/session/ui/ConnectionPanelTest.kt | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index e50cc2e3087..fb38dd51ad6 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -133,6 +133,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi - Extend `BasePlatformTestCase` to get a real IntelliJ Application and EDT in tests. The session package already uses `SessionControllerTestBase` which wraps this. - Do not mock the EDT or threading assertions — test against the real threading model. - Do not add production methods whose only purpose is test access. Prefer exercising the public API and inspecting the real Swing component tree in tests. +- Do not expose `internal` accessors, helper methods, or synthetic seams just so tests can inspect private implementation details. If a test needs this, either assert observable UI/action behavior or refactor the production API so the new seam has real product value. - For state-driven updates, assert that the component state matches after flushing coroutines and draining the EDT. - For retained Swing components, assert that expand/collapse, update, and no-op paths work correctly without rebuilding the component tree. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 8101e746534..ed563da8844 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -299,8 +299,6 @@ class ConnectionPanel( internal fun retryFocusable() = retry.isFocusable - internal fun recoveryActionTexts() = recoveryGroup().getChildren(null).mapNotNull { it.templatePresentation.text } - internal fun hasSeparator() = border != null internal fun maxExpandedHeight() = diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index a08002dfb47..deb5a8d94e8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -80,7 +80,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertEquals("Try again", panel.retryText()) } - fun `test retry popup group keeps lightweight retry and cli recovery actions`() { + fun `test retry popup group uses cli recovery actions`() { edt { panel.onEvent(SessionControllerEvent.ConnectionChanged.ShowError("CLI startup failed", null)) } @@ -89,7 +89,6 @@ class ConnectionPanelTest : SessionControllerTestBase() { .use { it.readText() } assertTrue(panel.retryVisible()) - assertTrue(panel.recoveryActionTexts().contains("Try again")) assertEquals("Kilo.CliGroup", ConnectionPanel.CLI_GROUP_ID) assertTrue(xml.contains("")) assertTrue(xml.contains(""))