fix(jetbrains): don't surface workspace fetch failures as IDE errors

Retry exhaustion for workspace/app data loads logged via IntelliJ's
Logger.error, which IntelliJ turns into an 'IDE Internal Error' popup.
Downgrade the exhausted-retry log to warn and enrich workspace fetch
failures with HTTP status and response body so the real cause (e.g. a
500 from the CLI /agent endpoint) is visible in diagnostics instead of
a generic crash-style dialog.
This commit is contained in:
kirillk
2026-08-18 12:23:23 -04:00
parent 91a337e31c
commit 254acc92a4
5 changed files with 61 additions and 6 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Avoid showing JetBrains internal error popups when Kilo workspace data fails to load, and include HTTP status and response details in diagnostics.
@@ -786,7 +786,7 @@ class KiloBackendAppService private constructor(
delay(RETRY_DELAY_MS)
}
}
log.error("$name: all $MAX_RETRIES attempts failed")
log.warn("$name: all $MAX_RETRIES attempts failed${last.error?.let { ": ${error(it)}" } ?: ""}")
return last
}
@@ -6,6 +6,10 @@ import ai.kilocode.backend.app.SseEvent
import ai.kilocode.backend.cli.KiloCliDataParser
import ai.kilocode.log.KiloLog
import ai.kilocode.jetbrains.api.client.DefaultApi
import ai.kilocode.jetbrains.api.infrastructure.ClientError
import ai.kilocode.jetbrains.api.infrastructure.ClientException
import ai.kilocode.jetbrains.api.infrastructure.ServerError
import ai.kilocode.jetbrains.api.infrastructure.ServerException
import ai.kilocode.jetbrains.api.model.Agent
import ai.kilocode.rpc.dto.SessionDto
import ai.kilocode.rpc.dto.SessionListDto
@@ -231,7 +235,8 @@ class KiloBackendWorkspace(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log.warn("Agents fetch failed: ${e.message}", e)
log.warn("Agents fetch failed: ${detail(e)}", e)
logResponseBody("agents", e)
FetchResult.fail("agents", e)
}
}
@@ -260,7 +265,8 @@ class KiloBackendWorkspace(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log.warn("Skills fetch failed: ${e.message}", e)
log.warn("Skills fetch failed: ${detail(e)}", e)
logResponseBody("skills", e)
FetchResult.fail("skills", e)
}
}
@@ -301,19 +307,34 @@ class KiloBackendWorkspace(
delay(RETRY_DELAY_MS)
}
}
log.error("$name: all $MAX_RETRIES attempts failed")
log.warn("$name: all $MAX_RETRIES attempts failed${last.error?.let { ": ${error(it)}" } ?: ""}")
return last
}
private fun setWorkspaceError(message: String, errors: List<LoadError>) {
log.warn("Workspace error [$directory]: $message")
val text = if (errors.isEmpty()) message else "$message [${errors.joinToString("; ") { error(it) }}]"
log.warn("Workspace error [$directory]: $text")
_state.value = KiloWorkspaceState.Error(message, errors)
}
private fun logResponseBody(resource: String, e: Exception) {
val body = body(e) ?: return
log.warn("$resource response body: $body")
}
private fun error(err: LoadError): String {
val status = err.status?.let { " status=$it" } ?: ""
val detail = err.detail?.let { " detail=$it" } ?: ""
return "${err.resource}$status$detail"
}
private data class FetchResult<T>(val value: T?, val error: LoadError?) {
companion object {
fun <T> ok(value: T) = FetchResult<T>(value, null)
fun <T> fail(resource: String, e: Exception? = null) = FetchResult<T>(null, LoadError(resource, detail = e?.message))
fun <T> fail(resource: String, e: Exception? = null) = FetchResult<T>(
null,
LoadError(resource, status = e?.let(::status), detail = e?.let(::detail)),
)
}
}
@@ -322,3 +343,23 @@ class KiloBackendWorkspace(
}
private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8)
private fun status(e: Exception): Int? = when (e) {
is ClientException -> e.statusCode
is ServerException -> e.statusCode
else -> null
}
private fun body(e: Exception): String? = when (e) {
is ClientException -> (e.response as? ClientError<*>)?.body?.toString()
is ServerException -> (e.response as? ServerError<*>)?.body?.toString()
else -> null
}
private fun detail(e: Exception): String? {
val body = body(e)
val status = status(e)
if (status != null && !body.isNullOrBlank()) return "HTTP $status: $body"
if (status != null) return "HTTP $status: ${e.message}"
return e.message
}
@@ -522,6 +522,8 @@ class KiloBackendAppServiceTest {
assertTrue(log.messages.any {
it.contains("App error: Failed to load required data") && it.contains("config")
})
assertTrue(log.messages.any { it.contains("WARN: config: all 3 attempts failed") })
assertTrue(log.messages.none { it.contains("ERROR: config: all 3 attempts failed") })
}
@Test
@@ -232,6 +232,7 @@ class KiloBackendWorkspaceTest {
@Test
fun `agents failure retries then transitions to Error`() = runBlocking {
mock.agentsStatus = 500
mock.agents = """{"error":"invalid agent config"}"""
val app = setup()
val ws = ready(app)
@@ -241,6 +242,12 @@ class KiloBackendWorkspaceTest {
val err = ws.state.value as KiloWorkspaceState.Error
assertTrue(err.message.contains("agents"))
val item = err.errors.single { it.resource == "agents" }
assertEquals(500, item.status)
assertTrue(item.detail?.contains("invalid agent config") == true)
assertTrue(log.messages.any { it.contains("agents response body:") && it.contains("invalid agent config") })
assertTrue(log.messages.any { it.contains("WARN: agents: all 3 attempts failed") })
assertTrue(log.messages.none { it.contains("ERROR: agents: all 3 attempts failed") })
}
@Test