fix(jetbrains): address recovery review feedback

This commit is contained in:
kirillk
2026-07-01 10:22:59 -04:00
parent 02c1177d6e
commit d5f9e53e73
6 changed files with 83 additions and 37 deletions
+1 -1
View File
@@ -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`.
@@ -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<Job> {
return listOfNotNull(
e.workspaceDirectory()?.let { service.refreshLocalConfigTarget(it) },
service.refreshGlobalConfigTarget(),
)
}
}
@@ -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 {
@@ -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() =
@@ -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<Unit>()
val gate = CompletableDeferred<Unit>()
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<Unit>()
rpc.beforeLocalConfigTarget = { gate.await() }
val call = CompletableDeferred<Unit>()
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<Unit>()
val gate = CompletableDeferred<Unit>()
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<Unit>()
rpc.beforeGlobalConfigTarget = { gate.await() }
val call = CompletableDeferred<Unit>()
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`() {
val local = CompletableDeferred<Unit>()
val global = CompletableDeferred<Unit>()
rpc.beforeLocalConfigTarget = { local.complete(Unit) }
rpc.beforeGlobalConfigTarget = { global.complete(Unit) }
service().workspace("/test")
waitFor { rpc.localConfigPathCalls == 1 && rpc.globalConfigPathCalls == 1 }
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<Unit>) = runBlocking {
withTimeout(5_000) { signal.await() }
}
private fun service(): KiloWorkspaceService = ApplicationManager.getApplication().getService(KiloWorkspaceService::class.java)
@@ -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("<group id=\"Kilo.CliGroup\" text=\"CLI\" popup=\"true\">"))
assertTrue(xml.contains("<reference ref=\"Kilo.Restart\"/>"))