wip(jetbrains): add generated API client, dual HTTP clients, and project-scoped RPC

- Generate Kotlin API client from the same OpenAPI spec as the VS Code extension
  using openapi-generator with jvm-okhttp4 + Moshi (bundled, no platform deps)
- Separate health checks (3s timeout) from API/SSE calls (no timeout) using two
  OkHttp clients, matching the VS Code extension's connection architecture
- Use ProjectId for all frontend↔backend RPC calls so the backend resolves the
  correct project service without scanning ProjectManager (JetBrains template pattern)
- Add KiloProjectService as project-level backend service scoping CLI API calls
  via the project's working directory
- Log CLI binary extraction path and startup command for easier debugging
This commit is contained in:
kirillk
2026-04-09 16:33:29 -04:00
parent f7c0c2658a
commit 70054f8225
14 changed files with 354 additions and 84 deletions
@@ -2,18 +2,88 @@ plugins {
alias(libs.plugins.rpc)
alias(libs.plugins.kotlin)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.openapi.generator)
}
kotlin {
jvmToolchain(21)
}
val generatedApi = layout.buildDirectory.dir("generated/openapi/src/main/kotlin")
sourceSets {
main {
resources.srcDir(layout.buildDirectory.dir("generated/cli"))
kotlin.srcDir(generatedApi)
}
}
openApiGenerate {
generatorName.set("kotlin")
library.set("jvm-okhttp4")
inputSpec.set("${rootDir}/../sdk/openapi.json")
outputDir.set(layout.buildDirectory.dir("generated/openapi").get().asFile.absolutePath)
packageName.set("ai.kilocode.jetbrains.api")
apiPackage.set("ai.kilocode.jetbrains.api.client")
modelPackage.set("ai.kilocode.jetbrains.api.model")
configOptions.set(mapOf(
"serializationLibrary" to "moshi",
"omitGradleWrapper" to "true",
"omitGradlePluginVersions" to "true",
"useCoroutines" to "false",
"sourceFolder" to "src/main/kotlin",
"enumPropertyNaming" to "UPPERCASE",
))
// Remap schema "File" so the generated class is not named java.io.File
modelNameMappings.set(mapOf(
"File" to "DiffFileInfo",
))
// Map empty anyOf references to kotlin.Any
typeMappings.set(mapOf(
"AnyOfLessThanGreaterThan" to "kotlin.Any",
"anyOf<>" to "kotlin.Any",
))
// Normalise OpenAPI 3.1 → 3.0-compatible patterns
openapiNormalizer.set(mapOf(
"SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING" to "true",
"SIMPLIFY_ONEOF_ANYOF" to "true",
))
generateApiTests.set(false)
generateModelTests.set(false)
generateApiDocumentation.set(false)
generateModelDocumentation.set(false)
}
// Fix openapi-generator 3.1.1 codegen bugs in generated Kotlin sources.
// - Boolean const enums: `enum class Foo(val value: kotlin.Boolean) { TRUE("true") }` → fix string→boolean
val fixGeneratedApi by tasks.registering {
dependsOn("openApiGenerate")
val dir = generatedApi
doLast {
dir.get().asFile.walkTopDown().filter { it.extension == "kt" }.forEach { file ->
var text = file.readText()
var changed = false
// Fix: enum Xxx(val value: kotlin.Boolean) { @Json(name = "true") TRUE("true") }
// → enum Xxx(val value: kotlin.Boolean) { @Json(name = "true") TRUE(true) }
val boolEnum = Regex(
"""(enum class \w+\(val value: kotlin\.Boolean\) \{[^}]*?@Json\(name = ")(true|false)("\) \w+\()"(true|false)"(\))"""
)
val replaced = boolEnum.replace(text) { m ->
"${m.groupValues[1]}${m.groupValues[2]}${m.groupValues[3]}${m.groupValues[4]}${m.groupValues[5]}"
}
if (replaced != text) {
text = replaced
changed = true
}
if (changed) file.writeText(text)
}
}
}
tasks.named("compileKotlin") {
dependsOn(fixGeneratedApi)
}
val cliDir = layout.buildDirectory.dir("generated/cli/cli")
val production = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false)
@@ -71,4 +141,6 @@ dependencies {
implementation(project(":shared"))
implementation(libs.okhttp)
implementation(libs.okhttp.sse)
implementation(libs.moshi)
implementation(libs.moshi.kotlin)
}
@@ -1,18 +0,0 @@
@file:Suppress("UnstableApiUsage")
package ai.kilocode.rpc
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.server.KiloConnectionService
import com.intellij.openapi.components.service
import kotlinx.coroutines.flow.Flow
class KiloBackendRpcApi : KiloRpcApi {
override suspend fun connect() {
service<KiloConnectionService>().connect()
}
override suspend fun state(): Flow<ConnectionStateDto> {
return service<KiloConnectionService>().stream()
}
}
@@ -0,0 +1,36 @@
@file:Suppress("UnstableApiUsage")
package ai.kilocode.rpc
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.rpc.dto.HealthDto
import ai.kilocode.server.KiloProjectService
import com.intellij.openapi.components.service
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProjectOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
/**
* Backend implementation of [KiloProjectRpcApi].
*
* Resolves the project from the [ProjectId] passed by the frontend
* and delegates to the project-level [KiloProjectService].
*/
class KiloProjectRpcApiImpl : KiloProjectRpcApi {
private fun resolve(id: ProjectId): KiloProjectService {
val project = id.findProjectOrNull()
?: throw IllegalStateException("Project not found for id: $id")
return project.service()
}
override suspend fun connect(projectId: ProjectId) =
resolve(projectId).connect()
override suspend fun state(projectId: ProjectId): Flow<ConnectionStateDto> =
resolve(projectId).stream()
override suspend fun health(projectId: ProjectId): HealthDto =
resolve(projectId).health()
}
@@ -5,10 +5,10 @@ package ai.kilocode.rpc
import com.intellij.platform.rpc.backend.RemoteApiProvider
import fleet.rpc.remoteApiDescriptor
internal class KiloBackendRpcApiProvider : RemoteApiProvider {
internal class KiloProjectRpcApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<KiloRpcApi>()) {
KiloBackendRpcApi()
remoteApi(remoteApiDescriptor<KiloProjectRpcApi>()) {
KiloProjectRpcApiImpl()
}
}
}
@@ -1,5 +1,6 @@
package ai.kilocode.server
import ai.kilocode.jetbrains.api.client.DefaultApi
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.rpc.dto.ConnectionStatusDto
import com.intellij.openapi.Disposable
@@ -17,16 +18,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.map
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import java.util.Base64
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
@@ -40,6 +40,16 @@ sealed class ConnectionState {
data class SseEvent(val type: String, val data: String)
/**
* App-level service managing the CLI server connection.
*
* Uses two separate OkHttp clients mirroring the VS Code architecture:
* - [apiClient]: no call/read timeout — used for the generated API client and SSE
* - [healthClient]: 3 s timeout — used only for `/global/health` polling
*
* The generated [DefaultApi] is configured with [apiClient] and exposed via [api]
* for typed access to all CLI server endpoints.
*/
@Service(Service.Level.APP)
class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
@@ -47,7 +57,6 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
private val LOG = Logger.getInstance(KiloConnectionService::class.java)
private const val HEARTBEAT_TIMEOUT_MS = 15_000L
private const val HEALTH_POLL_INTERVAL_MS = 10_000L
private const val HEALTH_TIMEOUT_MS = 3_000L
private const val RECONNECT_DELAY_MS = 250L
private val TYPE_REGEX = Regex(""""type"\s*:\s*"([^"]+)"""")
}
@@ -58,7 +67,12 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
private val _events = MutableSharedFlow<SseEvent>(extraBufferCapacity = 64)
val events: SharedFlow<SseEvent> = _events.asSharedFlow()
private var client: OkHttpClient? = null
/** Generated API client — null when disconnected. */
var api: DefaultApi? = null
private set
private var apiClient: OkHttpClient? = null
private var healthClient: OkHttpClient? = null
private var port = 0
private var password = ""
@@ -73,7 +87,6 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
suspend fun connect() {
if (_state.value is ConnectionState.Connected || _state.value is ConnectionState.Connecting) return
open()
}
@@ -82,32 +95,29 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
close()
processJob?.cancel()
healthJob?.cancel()
setState(ConnectionState.Connecting)
val cli = service<ServerManager>()
val processState = cli.init()
val result = cli.init()
if (processState is ServerManager.ServerState.Error) {
setState(ConnectionState.Error(processState.message))
if (result is ServerManager.ServerState.Error) {
setState(ConnectionState.Error(result.message))
return
}
val ready = processState as ServerManager.ServerState.Ready
val ready = result as ServerManager.ServerState.Ready
port = ready.port
password = ready.password
client = OkHttpClient.Builder()
.addInterceptor { chain ->
val auth = Base64.getEncoder().encodeToString("kilo:$password".toByteArray())
chain.proceed(
chain.request().newBuilder()
.header("Authorization", "Basic $auth")
.build()
)
}
.callTimeout(HEALTH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.build()
// Create dual OkHttp clients (bundled — no IntelliJ platform deps)
val ac = KiloHttpClients.api(password)
val hc = KiloHttpClients.health(password)
apiClient = ac
healthClient = hc
// Configure generated API client with the no-timeout api client
api = DefaultApi(basePath = "http://127.0.0.1:$port", client = ac)
startSse()
startHeartbeatWatcher()
@@ -118,7 +128,7 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
}
private fun startSse() {
val http = client ?: return
val http = apiClient ?: return
val request = Request.Builder()
.url("http://127.0.0.1:$port/global/event")
.header("Accept", "text/event-stream")
@@ -143,8 +153,8 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
override fun onEvent(src: EventSource, id: String?, type: String?, data: String) {
lastEvent.set(System.currentTimeMillis())
val eventType = type ?: extractType(data)
cs.launch { _events.emit(SseEvent(type = eventType, data = data)) }
val kind = type ?: extractType(data)
cs.launch { _events.emit(SseEvent(type = kind, data = data)) }
}
override fun onClosed(src: EventSource) {
@@ -215,7 +225,7 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
}
private fun checkHealth(): Boolean {
val http = client ?: return false
val http = healthClient ?: return false
return try {
val req = Request.Builder()
.url("http://127.0.0.1:$port/global/health")
@@ -238,25 +248,24 @@ class KiloConnectionService(private val cs: CoroutineScope) : Disposable {
}
private fun close() {
client?.let { http ->
http.dispatcher.executorService.shutdown()
http.connectionPool.evictAll()
}
client = null
api = null
apiClient?.let { KiloHttpClients.shutdown(it) }
apiClient = null
healthClient?.let { KiloHttpClients.shutdown(it) }
healthClient = null
}
private fun setState(next: ConnectionState) {
_state.value = next
}
private fun dto(state: ConnectionState): ConnectionStateDto {
return when (state) {
private fun dto(state: ConnectionState): ConnectionStateDto =
when (state) {
ConnectionState.Disconnected -> ConnectionStateDto(ConnectionStatusDto.DISCONNECTED)
ConnectionState.Connecting -> ConnectionStateDto(ConnectionStatusDto.CONNECTING)
is ConnectionState.Connected -> ConnectionStateDto(ConnectionStatusDto.CONNECTED)
is ConnectionState.Error -> ConnectionStateDto(ConnectionStatusDto.ERROR, state.message)
}
}
private fun extractType(data: String): String =
TYPE_REGEX.find(data)?.groupValues?.get(1) ?: "unknown"
@@ -0,0 +1,58 @@
package ai.kilocode.server
import okhttp3.ConnectionPool
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import java.util.Base64
import java.util.concurrent.TimeUnit
/**
* Factory for the two OkHttp clients used by the plugin.
*
* Mirrors the VS Code architecture:
* - [api] client has no call/read timeout (streaming ops like prompt/SSE can run long)
* - [health] client has a short 3 s timeout and a small dedicated connection pool
*
* Both clients bundle Basic Auth via an interceptor and are fully independent
* of any IntelliJ-platform-provided HTTP stack.
*/
object KiloHttpClients {
private const val CONNECT_TIMEOUT_MS = 10_000L
private const val HEALTH_TIMEOUT_MS = 3_000L
/** API client — no call/read timeout (SSE and long-running ops). */
fun api(password: String): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(auth(password))
.connectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.callTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
/** Health client — short timeout, dedicated connection pool. */
fun health(password: String): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(auth(password))
.connectTimeout(HEALTH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.callTimeout(HEALTH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.connectionPool(ConnectionPool(1, 30, TimeUnit.SECONDS))
.build()
/** Shut down both dispatcher and connection pool for the given client. */
fun shutdown(client: OkHttpClient) {
client.dispatcher.executorService.shutdown()
client.connectionPool.evictAll()
}
private fun auth(password: String): Interceptor {
val header = "Basic ${Base64.getEncoder().encodeToString("kilo:$password".toByteArray())}"
return Interceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("Authorization", header)
.build()
)
}
}
}
@@ -0,0 +1,63 @@
package ai.kilocode.server
import ai.kilocode.jetbrains.api.client.DefaultApi
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.rpc.dto.HealthDto
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
* Project-level backend service that delegates to the app-level
* [KiloConnectionService] and scopes CLI API calls to this project's
* working directory.
*
* The VS Code extension likewise scopes calls via `x-kilo-directory`.
* In the JetBrains plugin this is achieved by passing the directory
* parameter to each generated API method.
*/
@Service(Service.Level.PROJECT)
class KiloProjectService(
private val project: Project,
private val cs: CoroutineScope,
) {
private val connection: KiloConnectionService
get() = service()
/** Project working directory sent as the `directory` parameter. */
val directory: String
get() = project.basePath ?: ""
/** Connection state (delegates to app-level service). */
val state: StateFlow<ConnectionState>
get() = connection.state
/** Connection state mapped to DTO for RPC transport. */
fun stream() = connection.stream()
/** Ensure the CLI backend is running and connected. */
suspend fun connect() = connection.connect()
/**
* The generated API client, or null when disconnected.
*
* Callers should pass [directory] to each API method's `directory`
* parameter to scope requests to this project.
*/
val api: DefaultApi?
get() = connection.api
/**
* One-shot health check via the generated API client.
* Returns [HealthDto] or throws if not connected / server unreachable.
*/
suspend fun health(): HealthDto {
val client = api ?: throw IllegalStateException("Not connected")
val response = client.globalHealth()
return HealthDto(healthy = true, version = response.version)
}
}
@@ -78,6 +78,7 @@ class ServerManager(private val cs: CoroutineScope) : Disposable {
private suspend fun start(): ServerState {
return try {
val path = extractCli()
LOG.info("CLI binary path: ${path.absolutePath} (size=${path.length()} bytes)")
withTimeout(STARTUP_TIMEOUT_MS) {
spawn(path)
}
@@ -136,13 +137,16 @@ class ServerManager(private val cs: CoroutineScope) : Disposable {
put("KILO_APP_NAME", "kilo-code")
}
val builder = ProcessBuilder(cli.absolutePath, "serve", "--port", "0")
val cmd = listOf(cli.absolutePath, "serve", "--port", "0")
val builder = ProcessBuilder(cmd)
builder.environment().clear()
builder.environment().putAll(env)
builder.redirectErrorStream(false)
LOG.info("Spawning: ${cli.absolutePath} serve --port 0")
LOG.info("Starting CLI: ${cmd.joinToString(" ")}")
LOG.info("CLI env: KILO_CLIENT=jetbrains KILO_PLATFORM=jetbrains KILO_APP_NAME=kilo-code")
val proc = builder.start()
LOG.info("CLI process started (pid=${proc.pid()})")
process = proc
install(proc)
@@ -6,6 +6,6 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.backend.remoteApiProvider implementation="ai.kilocode.rpc.KiloBackendRpcApiProvider"/>
<platform.rpc.backend.remoteApiProvider implementation="ai.kilocode.rpc.KiloProjectRpcApiProvider"/>
</extensions>
</idea-plugin>
@@ -2,12 +2,14 @@
package ai.kilocode
import ai.kilocode.rpc.KiloRpcApi
import ai.kilocode.rpc.KiloProjectRpcApi
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.rpc.dto.ConnectionStatusDto
import ai.kilocode.rpc.dto.HealthDto
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.platform.project.projectId
import fleet.rpc.client.durable
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
@@ -18,6 +20,13 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* Frontend project-level service for Kilo CLI interaction.
*
* Communicates with the backend via [KiloProjectRpcApi], passing
* [project.projectId] on every call so the backend can resolve the
* correct project-level service without scanning ProjectManager.
*/
@Service(Service.Level.PROJECT)
class KiloApiService(
private val project: Project,
@@ -31,7 +40,9 @@ class KiloApiService(
val state: StateFlow<ConnectionStateDto> = flow {
durable {
KiloRpcApi.getInstance().state().collect { emit(it) }
KiloProjectRpcApi.getInstance()
.state(project.projectId())
.collect { emit(it) }
}
}.stateIn(cs, SharingStarted.Eagerly, init)
@@ -39,11 +50,18 @@ class KiloApiService(
if (!started.compareAndSet(false, true)) return
cs.launch {
durable {
KiloRpcApi.getInstance().connect()
KiloProjectRpcApi.getInstance().connect(project.projectId())
}
}
}
/** One-shot health check. Returns null on failure. */
suspend fun health(): HealthDto? = try {
durable { KiloProjectRpcApi.getInstance().health(project.projectId()) }
} catch (_: Exception) {
null
}
fun watch(fn: (String) -> Unit): Job {
val mgr = ToolWindowManager.getInstance(project)
return cs.launch {
@@ -55,8 +73,8 @@ class KiloApiService(
}
}
private fun text(state: ConnectionStateDto): String {
return when (state.status) {
private fun text(state: ConnectionStateDto): String =
when (state.status) {
ConnectionStatusDto.DISCONNECTED -> KiloBundle.message("toolwindow.status.disconnected")
ConnectionStatusDto.CONNECTING -> KiloBundle.message("toolwindow.status.connecting")
ConnectionStatusDto.CONNECTED -> KiloBundle.message("toolwindow.status.connected")
@@ -65,5 +83,4 @@ class KiloApiService(
state.error ?: KiloBundle.message("toolwindow.error.unknown"),
)
}
}
}
@@ -6,10 +6,14 @@ kotlin-jvm-plugin = "2.1.20"
kotlin-serialization-plugin = "2.1.20"
kotlin-serialization = "1.7.3"
okhttp = "4.12.0"
moshi = "1.15.1"
openapi-generator = "7.12.0"
[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse", version.ref = "okhttp" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
[plugins]
intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-gradle-plugin" }
@@ -17,3 +21,4 @@ rpc = { id = "rpc", version.ref = "intellij-rpc-plugin" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-jvm-plugin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-jvm-plugin" }
openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" }
@@ -0,0 +1,36 @@
package ai.kilocode.rpc
import ai.kilocode.rpc.dto.ConnectionStateDto
import ai.kilocode.rpc.dto.HealthDto
import com.intellij.platform.project.ProjectId
import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.flow.Flow
/**
* Project-scoped RPC API exposed from backend to frontend.
*
* Every method takes a [ProjectId] as its first parameter, following the
* JetBrains modular plugin template pattern. The frontend obtains the ID
* via `project.projectId()` and the backend resolves the project via
* `projectId.findProjectOrNull()`.
*/
@Rpc
interface KiloProjectRpcApi : RemoteApi<Unit> {
companion object {
suspend fun getInstance(): KiloProjectRpcApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<KiloProjectRpcApi>())
}
}
/** Ensure the CLI backend is running and connected. */
suspend fun connect(projectId: ProjectId)
/** Observe connection state changes. */
suspend fun state(projectId: ProjectId): Flow<ConnectionStateDto>
/** One-shot health check against /global/health. */
suspend fun health(projectId: ProjectId): HealthDto
}
@@ -1,21 +0,0 @@
package ai.kilocode.rpc
import ai.kilocode.rpc.dto.ConnectionStateDto
import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.flow.Flow
@Rpc
interface KiloRpcApi : RemoteApi<Unit> {
companion object {
suspend fun getInstance(): KiloRpcApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<KiloRpcApi>())
}
}
suspend fun connect()
suspend fun state(): Flow<ConnectionStateDto>
}
@@ -0,0 +1,9 @@
package ai.kilocode.rpc.dto
import kotlinx.serialization.Serializable
@Serializable
data class HealthDto(
val healthy: Boolean,
val version: String,
)