Skip to content

feat(jetbrains): backend architecture, OpenAPI client, and test suite#8849

Open
kirillk wants to merge 21 commits intomainfrom
lime-dormouse
Open

feat(jetbrains): backend architecture, OpenAPI client, and test suite#8849
kirillk wants to merge 21 commits intomainfrom
lime-dormouse

Conversation

@kirillk
Copy link
Copy Markdown
Contributor

@kirillk kirillk commented Apr 13, 2026

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:

┌──────────────────────────────────────────────────────────────────────┐
│                        JetBrains IDE (Host)                          │
│                                                                      │
│  ┌─────────────────────────┐       ┌──────────────────────────────┐  │
│  │       frontend/          │  RPC  │          backend/            │  │
│  │                          │◄─────►│                              │  │
│  │  KiloToolWindowFactory   │       │  KiloBackendAppService       │  │
│  │  KiloAppService (UI)     │       │    ├── KiloBackendCliManager │  │
│  │  Actions (restart, etc.) │       │    └── KiloConnectionService │  │
│  │                          │       │                              │  │
│  └─────────────────────────┘       └──────────┬───────────────────┘  │
│                                                │                      │
│  ┌─────────────────────────┐                   │                      │
│  │       shared/            │                   │                      │
│  │                          │                   │                      │
│  │  KiloAppRpcApi (@Rpc)    │                   │                      │
│  │  KiloAppStateDto         │                   │                      │
│  │  HealthDto               │                   │                      │
│  └─────────────────────────┘                   │                      │
└────────────────────────────────────────────────┼──────────────────────┘
                                                 │
                                                 │ HTTP + SSE
                                                 │ (OkHttp, Basic Auth)
                                                 ▼
                                    ┌────────────────────────┐
                                    │   kilo serve --port 0  │
                                    │   (CLI subprocess)     │
                                    │                        │
                                    │  GET /global/health    │
                                    │  GET /global/config    │
                                    │  GET /global/event     │ ◄── SSE stream
                                    │  GET /kilo/profile     │
                                    │  GET /kilo/notifications│
                                    └────────────────────────┘

Backend lifecycle

KiloBackendAppService (orchestrator, Mutex-serialized)
  │
  ├── KiloBackendCliManager (implements CliServer)
  │     • Extracts CLI binary from JAR resources
  │     • Spawns `kilo serve --port 0`
  │     • Reads port from stdout regex
  │     • Manages shutdown hooks and process tree
  │
  └── KiloConnectionService
        • Dual OkHttp clients (api: no timeout, health: 3s timeout)
        • SSE stream via OkHttp EventSource
        • Heartbeat monitoring (15s timeout)
        • Health polling (10s interval)
        • Automatic reconnection with process monitoring

State machine

  Disconnected ──► Connecting ──► Loading ──► Ready
       ▲                │            │          │
       │                ▼            ▼          │
       └──────────── Error ◄─────────┘          │
       │                                        │
       └────────────────────────────────────────┘
                    (restart/reinstall)

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 using openapi-generator with jvm-okhttp4 + kotlinx_serialization. The generator produces several codegen bugs that FixGeneratedApiTask patches automatically:

# Bug Fix
1 const: true/false generates broken single-value boolean enums Replace with plain kotlin.Boolean
2 HashMap<...>()() — double parentheses Remove extra ()
3 kotlin.Double("5000") — private constructor Convert to 5000.0 literal
4 Missing @Contextual on kotlin.Any fields Add annotation for kotlinx.serialization
5 response.body assumed non-null Add null guard in ApiClient.kt
6 No serializer for Any type Inject AnySerializer backed by JsonElement
7 Empty anyOf wrapper classes Replace with JsonElement

Test suite

43 tests across 5 classes, running as plain JVM/JUnit — no IntelliJ test framework required.

Test infrastructure

  • MockCliServer — Raw ServerSocket HTTP 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 — Implements CliServer interface, delegates to MockCliServer instead 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). KiloBackendCliManager implements it; tests use FakeCliServer.
  • KiloLog — Abstracts logging. IntellijLog wraps com.intellij.openapi.diagnostic.Logger; tests use TestLog.

KiloBackendAppService exposes an internal fun create() factory for tests to construct instances with injected fakes, while the public constructor remains compatible with IntelliJ service injection.

Test coverage

Class Tests What's covered
KiloBackendHttpClientsTest 6 Auth headers, timeouts, connection pool shutdown
KiloAppStateTest 8 Sealed class subtypes, LoadProgress, LoadError, ProfileResult
ApiModelSerializationTest 12 JSON roundtrips for Config, Health, Notifications, Profile with edge cases (nulls, unknown fields, minimal JSON)
KiloConnectionServiceTest 7 Connect lifecycle, SSE events, error states, reinstall/forceExtract, dispose
KiloBackendAppServiceTest 10 Full lifecycle to Ready, retry on failure, profile 401 tolerance, profile 500 error surfacing, health flag forwarding, SSE config refresh, health check

Bugs found and fixed

Three bugs were caught during testing and code review:

  1. dispose() race in KiloConnectionService — OkHttp SSE onFailure callback fires on a background thread after dispose() sets state to Disconnected, overwriting it with Error("Socket closed"). Fixed by adding a disposed flag that gates all setState calls.

  2. health() hardcoded healthy = trueglobalHealth() returns the server health flag, but the RPC response always reported success. If the CLI answered with healthy = false, the frontend would still see a healthy state. Fixed to forward response.healthy.

  3. fetchProfile() swallowed non-auth errors — The catch block treated every exception (500, timeout, deserialization) as NOT_LOGGED_IN, letting the app reach Ready while hiding real backend problems. Fixed to only treat 401 as not-logged-in; other failures now surface as Error state with a profile LoadError.

kirillk added 14 commits April 10, 2026 13:10
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.
…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.
@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot bot commented Apr 13, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/KiloBackendAppService.kt 117 health() hardcodes healthy = true instead of forwarding the server response.
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/KiloBackendAppService.kt 225 fetchProfile() treats every exception as NOT_LOGGED_IN, masking non-auth failures and allowing a false Ready state.

Fix these issues in Kilo Cloud

Other Observations (not in diff)

No additional issues found outside the diff.

Files Reviewed (3 files)
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/KiloBackendAppService.kt - 2 carried-forward issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/KiloAppStateTest.kt - 0 issues in incremental diff
  • packages/kilo-jetbrains/gradle/libs.versions.toml - 0 issues in incremental diff

Reviewed by gpt-5.4-20260305 · 819,453 tokens

kirillk added 7 commits April 13, 2026 09:29
…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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems quite hacky. Is the current version of openapi-generator really that problematic? If the version changes this might break?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants