feat(jetbrains): backend architecture, OpenAPI client, and test suite#8849
feat(jetbrains): backend architecture, OpenAPI client, and test suite#8849
Conversation
Introduce KiloAppService that owns ServerManager and KiloConnectionService as plain classes instead of separate services. On CLI connect, it loads global data (profile, config, notifications) in parallel and routes SSE events for config reloads. KiloProjectService now delegates to KiloAppService instead of KiloConnectionService directly.
…d move to backend package Rename server package to ai.kilocode.backend and apply consistent KiloBackend* prefix: KiloBackendAppService, KiloBackendCliManager, KiloBackendConnectionService, KiloBackendHttpClients, KiloBackendProjectService.
Move backend RPC classes to ai.kilocode.backend.rpc. Move frontend classes to ai.kilocode.client with subpackages for actions and plugin. Rename frontend KiloApiService to KiloAppService. Update XML registrations to match new fully-qualified class names.
…e ProjectId The RPC API is entirely project-neutral — all operations delegate to the app-level KiloBackendAppService. Remove the unused ProjectId parameter from all methods and rename to KiloAppRpcApi to reflect this. Move health() from KiloBackendProjectService to KiloBackendAppService and have the RPC impl delegate directly to the app service.
Promote KiloAppService from project-level to app-level service since it holds no project state. Change watch() to pass raw ConnectionStateDto instead of pre-formatted text, letting callers handle EDT scheduling and display conversion.
Lift concurrency control to KiloBackendAppService with a single Mutex that serializes connect/restart/reinstall/reconnect. Remove Mutex and Deferred from KiloBackendCliManager. Add onReconnect callback to KiloConnectionService so dead-process restarts go through the mutex. Fix process leak on startup timeout by killing orphaned CLI processes in the catch block. Skip redundant reconnects when already connected.
…r details Introduce KiloAppState lifecycle: Disconnected → Connecting → Loading → Ready → Error. Config and notifications are required (retried 3×), profile 401 is treated as not-logged-in (not an error). Each failed fetch captures HTTP status code and error detail from ClientException/ServerException. The tool window displays granular loading progress per resource and detailed error information including HTTP status and response messages.
…deserialization Add findBun() to build.gradle.kts to probe common install locations (/opt/homebrew/bin, ~/.bun/bin, etc.) so localCli task works even when Gradle daemon's PATH doesn't include bun. Fix Config.model and Config.small_model deserialization: the spec defines these as anyOf[string, null] but codegen produced empty wrapper classes. Extend fixGeneratedApi to delete ConfigModel/ConfigSmallModel and replace field types with kotlin.String?. Add response body logging on fetch failures for easier debugging.
…atic inner class error
…erialization Upgrade openapi-generator 7.12.0 → 7.21.0 and switch the generated API client serialization library from Moshi to kotlinx.serialization to unify with the RPC DTOs in shared/ which already use kotlinx. - Extract PrepareLocalCliTask to buildSrc/ to fix Gradle configuration cache error (non-static inner class) - Remove moshi/moshi-kotlin deps, add kotlinx-serialization-json - Map bare number types to kotlin.Double (avoids BigDecimal + @contextual) - Rewrite fixGeneratedApi task for 7 kotlinx codegen bugs: boolean const enums, double-parens on HashMap classes, private Double constructor, missing @contextual on Any, nullable OkHttp body, AnySerializer for dynamic JSON, and empty anyOf wrapper classes replaced with JsonElement
Extract all custom Gradle task classes (FixGeneratedApiTask, PrepareLocalCliTask, CheckCliTask) into a build-tasks/ composite build exposed via includeBuild. This keeps backend/build.gradle.kts purely declarative and co-locates build logic in its own compilable module.
…uild-tasks plugin Remove box-drawing section comments from build scripts, task classes, and KiloBackendAppService. Simplify build-tasks plugin ID and fix circular property evaluation in CheckCliTask configuration.
Extract CliServer interface and KiloLog abstraction to decouple backend classes from IntelliJ platform APIs, enabling tests that run as plain JVM/JUnit without the IDE test framework. Includes a raw-socket mock HTTP+SSE server, 41 tests covering connection lifecycle, data loading, retry logic, serialization roundtrips, and HTTP client configuration.
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/KiloBackendAppService.kt
Outdated
Show resolved
Hide resolved
...ages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt
Show resolved
Hide resolved
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Fix these issues in Kilo Cloud Other Observations (not in diff)No additional issues found outside the diff. Files Reviewed (3 files)
Reviewed by gpt-5.4-20260305 · 819,453 tokens |
…lback to 401 health() was hardcoding healthy=true instead of forwarding the server's response. fetchProfile() was catching all exceptions as NOT_LOGGED_IN, silently hiding 500s, timeouts, and deserialization failures. Now only 401 is treated as not-logged-in; other failures surface as Error state with a profile LoadError.
…lin 2.1.x) openapi-generator is already at the latest version (7.21.0). kotlinx-serialization 1.9.0+ requires Kotlin 2.2.0, so 1.8.1 is the correct ceiling for our Kotlin 2.1.20 toolchain.
|
|
||
| private suspend fun fetchProfile(): ProfileResult { | ||
| val client = connection.api ?: return ProfileResult.NOT_LOGGED_IN | ||
| return try { |
There was a problem hiding this comment.
All of those try catch statements smell to me. This catches and rethrows expeceptions across 4-5 layers. I think we are mixing here exceptions for control flow and and result types. Can't we use result types in every layer all the way through?
| * On success, transitions to [KiloAppState.Ready]. | ||
| * On failure of required data, transitions to [KiloAppState.Error]. | ||
| */ | ||
| private fun load() { |
There was a problem hiding this comment.
Should load run under the mutex too? Is this a realistic race?
connect() acquires mutex, calls connection.connect(), releases mutex
Connection succeeds, the init{} collector fires load()
load() spawns loader = cs.launch { ... } — this coroutine is now running freely on a dispatcher thread, writing to config, profile, _appState
Meanwhile, someone calls restart(), acquires the mutex, calls clear()
clear() does loader?.cancel() and then config = null
But the loader coroutine is between config = result.value and config!! — cancellation hasn't been checked yet because cancellation is cooperative, it only triggers at the next suspension point
The loader reads config!!, which is now null → NPE
| * Mark the given process as exited and clear state. | ||
| * Called from the process monitor when the CLI process dies. | ||
| */ | ||
| override fun exited(proc: Process) { |
There was a problem hiding this comment.
On the top you state all public * methods are called under its mutex so no internal synchronization is needed in a comment, however this one is public too.
| * uncompilable or runtime-broken Kotlin when using kotlinx.serialization. | ||
| * | ||
| * Fixes applied: | ||
| * 1. Boolean const enums — `const: true`/`false` produce broken single-value |
There was a problem hiding this comment.
This seems quite hacky. Is the current version of openapi-generator really that problematic? If the version changes this might break?
Summary
Implements the JetBrains plugin backend: CLI server lifecycle, connection management, data loading, and a comprehensive IntelliJ-independent test suite. Also adds OpenAPI client generation with post-processing fixes for kotlinx.serialization compatibility.
Architecture
The plugin uses IntelliJ's split-mode architecture with three Gradle modules:
Backend lifecycle
State machine
The Loading phase fetches config, notifications, and profile in parallel. Config and notifications are required (retried 3x). Profile is optional — 401 (not logged in) is fine, but other failures (500, timeout) surface as errors.
OpenAPI client generation
The CLI server HTTP API is defined in
packages/sdk/openapi.json. We generate a Kotlin client usingopenapi-generatorwithjvm-okhttp4+kotlinx_serialization. The generator produces several codegen bugs thatFixGeneratedApiTaskpatches automatically:const: true/falsegenerates broken single-value boolean enumskotlin.BooleanHashMap<...>()()— double parentheses()kotlin.Double("5000")— private constructor5000.0literal@Contextualonkotlin.Anyfieldsresponse.bodyassumed non-nullApiClient.ktAnytypeAnySerializerbacked byJsonElementanyOfwrapper classesJsonElementTest suite
43 tests across 5 classes, running as plain JVM/JUnit — no IntelliJ test framework required.
Test infrastructure
MockCliServer— RawServerSocketHTTP server simulating the CLI. Handles REST endpoints with configurable JSON responses and provides full control over the SSE stream (pushEvent,closeSse,awaitSseConnection). Supports restart (stop/start cycles).FakeCliServer— ImplementsCliServerinterface, delegates toMockCliServerinstead of spawning a real process.TestLog— Captures log messages for assertions without IntelliJ Logger.Interface seams for testability
Two interfaces were extracted from production code to decouple from IntelliJ APIs:
CliServer— Abstracts CLI process lifecycle (init,stop,dispose,process).KiloBackendCliManagerimplements it; tests useFakeCliServer.KiloLog— Abstracts logging.IntellijLogwrapscom.intellij.openapi.diagnostic.Logger; tests useTestLog.KiloBackendAppServiceexposes aninternal fun create()factory for tests to construct instances with injected fakes, while the public constructor remains compatible with IntelliJ service injection.Test coverage
KiloBackendHttpClientsTestKiloAppStateTestApiModelSerializationTestKiloConnectionServiceTestKiloBackendAppServiceTestBugs found and fixed
Three bugs were caught during testing and code review:
dispose()race inKiloConnectionService— OkHttp SSEonFailurecallback fires on a background thread afterdispose()sets state toDisconnected, overwriting it withError("Socket closed"). Fixed by adding adisposedflag that gates allsetStatecalls.health()hardcodedhealthy = true—globalHealth()returns the server health flag, but the RPC response always reported success. If the CLI answered withhealthy = false, the frontend would still see a healthy state. Fixed to forwardresponse.healthy.fetchProfile()swallowed non-auth errors — The catch block treated every exception (500, timeout, deserialization) asNOT_LOGGED_IN, letting the app reachReadywhile hiding real backend problems. Fixed to only treat 401 as not-logged-in; other failures now surface asErrorstate with aprofileLoadError.