feat(jetbrains): log CLI download/bundled mode and mark bundled Core version

Backend now logs a clear one-line mode statement when resolving Core
(BUNDLED vs DOWNLOAD), and logs distinctly when a cached/extracted
binary is reused so no download or extraction happens. Adds a
cliBundled() RPC so the frontend Core-info popup can prefix the
version with "Bundled" when Core wasn't downloaded.
This commit is contained in:
kirillk
2026-08-05 09:37:33 -04:00
parent 6a8302881d
commit a340d61716
12 changed files with 81 additions and 7 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": minor
---
Log whether the JetBrains plugin downloads Core or uses the bundled/cached version, and mark the Core version shown in the popup as "Bundled" when it wasn't downloaded.
@@ -126,14 +126,18 @@ class KiloBackendCliManager(
private suspend fun resolveCli(onProgress: (CliDownload) -> Unit): File {
val force = forceExtract
forceExtract = false
val version = KiloProps.cliVersion()
val platform = KiloCliPlatform.current()
if (KiloRepoCli.available()) {
if (force) log.info("Force re-extracting bundled CLI ${KiloProps.cliVersion()}")
if (force) log.info("Force re-extracting bundled CLI $version")
log.info("Kilo CLI mode: BUNDLED — using CLI $version ($platform) shipped in the plugin; no download needed")
val cli = KiloRepoCli.extract(force)
onProgress(CliDownload(100, KiloProps.cliVersion(), KiloCliPlatform.current()))
onProgress(CliDownload(100, version, platform))
return cli
}
if (force) log.info("Force re-downloading CLI ${KiloProps.cliVersion()}")
return KiloCliDownloader(log = log).resolve(KiloProps.cliVersion(), force, onProgress)
if (force) log.info("Force re-downloading CLI $version")
log.info("Kilo CLI mode: DOWNLOAD — resolving CLI $version ($platform) from the GitHub release")
return KiloCliDownloader(log = log).resolve(version, force, onProgress)
}
// Must be called from a background thread — devStorageEnv() performs blocking I/O (mkdirs).
@@ -112,7 +112,7 @@ class KiloCliDownloader(
"completeExists=${done.isFile} digestValid=$valid exe=${exe.absolutePath} complete=${done.absolutePath}"
)
if (!exe.isFile || !valid) return null
log.info("Using cached Kilo CLI $version for $platform at ${exe.absolutePath}")
log.info("Kilo CLI $version ($platform) already cached at ${exe.absolutePath}; skipping download and extraction")
if (!SystemInfo.isWindows) exe.setExecutable(true)
prune(version)
return exe
@@ -36,10 +36,12 @@ object KiloRepoCli {
val exe = File(root, "$platform/bin/${KiloCliPlatform.exe()}")
val done = File(root, ".complete")
if (!force && done.isFile && exe.isFile) {
log.info("Bundled Kilo CLI ${KiloProps.cliVersion()} ($platform) already extracted at ${exe.absolutePath}; skipping extraction")
if (!SystemInfo.isWindows) exe.setExecutable(true)
if (cleanup) prune(root)
return@withContext exe
}
log.info("Extracting bundled Kilo CLI ${KiloProps.cliVersion()} ($platform) into ${root.absolutePath}")
if (root.exists() && !root.deleteRecursively()) {
throw IllegalStateException("Failed to delete local repo CLI under ${root.absolutePath}")
@@ -11,6 +11,7 @@ import ai.kilocode.backend.app.LoadProgress
import ai.kilocode.backend.app.ProfileResult
import ai.kilocode.backend.cli.KiloCliPlatform
import ai.kilocode.backend.cli.KiloProps
import ai.kilocode.backend.cli.KiloRepoCli
import ai.kilocode.jetbrains.api.model.KiloProfile200Response
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.KiloAppRpcApi
@@ -57,6 +58,8 @@ class KiloAppRpcApiImpl : KiloAppRpcApi {
override suspend fun cliPlatform(): String = KiloCliPlatform.current()
override suspend fun cliBundled(): Boolean = KiloRepoCli.available()
override suspend fun retry() = app.retry()
override suspend fun restart() = app.restart()
@@ -70,7 +70,7 @@ class KiloCliDownloaderTest {
assertEquals(cli.absolutePath, cached.absolutePath)
assertEquals(1, server.requestCount)
assertTrue(cachedProgress.isEmpty())
assertContains(log.messages, "INFO: Using cached Kilo CLI 1.2.3 for ${KiloCliPlatform.current()} at ${cli.absolutePath}")
assertContains(log.messages, "INFO: Kilo CLI 1.2.3 (${KiloCliPlatform.current()}) already cached at ${cli.absolutePath}; skipping download and extraction")
File(cli.parentFile.parentFile, ".complete").writeText("ok\n")
server.enqueue(MockResponse().setResponseCode(200).setBody(Buffer().write(bytes)))
@@ -15,8 +15,10 @@ class CoreInfoAction : AnAction(), DumbAware {
val app = service<KiloAppService>()
val info = app.core
if (info == null) app.fetchCoreInfoAsync()
app.fetchBundledAsync()
val key = if (app.bundled == true) "action.Kilo.CoreInfo.bundled" else "action.Kilo.CoreInfo.text"
e.presentation.text = info?.let {
KiloBundle.message("action.Kilo.CoreInfo.text", it.version, it.platform)
KiloBundle.message(key, it.version, it.platform)
} ?: KiloBundle.message("action.Kilo.CoreInfo.loading")
e.presentation.description = KiloBundle.message("action.Kilo.CoreInfo.description")
e.presentation.isEnabled = false
@@ -58,6 +58,18 @@ class KiloAppService internal constructor(
val version: String? get() = info?.version
/**
* Whether the running Core is bundled in the plugin (true) or downloaded (false).
* Null until fetched. This is a static property of the plugin build, so it is
* fetched once via RPC independently of the download-progress state.
*/
@Volatile
private var bundledFlag: Boolean? = null
private val bundledLock = Any()
private var bundledJob: Job? = null
val bundled: Boolean? get() = bundledFlag
/**
* App-lifetime scope for fire-and-forget work that must outlive transient UIs such as the
* settings dialog (whose own scope is cancelled the moment it closes on OK).
@@ -152,6 +164,7 @@ class KiloAppService internal constructor(
platform = call { cliPlatform() },
)
info = next
bundledFlag = call { cliBundled() }
next
} catch (e: Exception) {
LOG.warn("core info failed", e)
@@ -200,6 +213,26 @@ class KiloAppService internal constructor(
fetchCoreInfoAsync { done(it?.version) }
}
/** Fetch whether the running Core is bundled and cache it. Deduped and fetched once. */
fun fetchBundledAsync() {
if (bundledFlag != null) return
synchronized(bundledLock) {
if (bundledFlag != null || bundledJob != null) return
bundledJob = cs.launch {
val value = try {
call { cliBundled() }
} catch (e: Exception) {
LOG.warn("core bundled check failed", e)
null
}
synchronized(bundledLock) {
if (value != null) bundledFlag = value
bundledJob = null
}
}
}
}
fun refreshModelFavoritesAsync() {
cs.launch {
try {
@@ -731,6 +731,7 @@ action.Kilo.Reinstall.text=Reinstall
action.Kilo.Reinstall.cli.text=Reinstall Core
action.Kilo.Reinstall.description=Download a fresh Core binary and restart
action.Kilo.CoreInfo.text=Core v{0} • Architecture: {1}
action.Kilo.CoreInfo.bundled=Bundled Core v{0} • Architecture: {1}
action.Kilo.CoreInfo.loading=Core details loading...
action.Kilo.CoreInfo.description=Kilo Core version and architecture
action.Kilo.Session.Open.text=Open
@@ -126,6 +126,21 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() {
assertEquals("Core v1.2.3 • Architecture: darwin-arm64", event.presentation.text)
}
fun `test core info action marks bundled core`() {
appRpc.cliVersion = "1.2.3"
appRpc.cliPlatform = "darwin-arm64"
appRpc.cliBundled = true
ApplicationManager.getApplication().executeOnPooledThread {
runBlocking { app().coreInfo() }
}.get()
val action = CoreInfoAction()
val event = event(action)
update(action, event)
assertEquals("Bundled Core v1.2.3 • Architecture: darwin-arm64", event.presentation.text)
}
fun `test local config action says open when target exists`() {
rpc.localConfigPath = "/test/.kilo/kilo.jsonc"
rpc.localConfigDisplayPath = "~/.kilo/kilo.jsonc"
@@ -37,6 +37,7 @@ class FakeAppRpcApi : KiloAppRpcApi {
var health = HealthDto(healthy = true, version = "1.0.0")
var cliVersion = "1.0.0"
var cliPlatform = "darwin-arm64"
var cliBundled = false
var cliInfoGate: CompletableDeferred<Unit>? = null
var cliInfoError: Exception? = null
var cliVersionCalls = 0
@@ -94,6 +95,11 @@ class FakeAppRpcApi : KiloAppRpcApi {
return cliPlatform
}
override suspend fun cliBundled(): Boolean {
assertNotEdt("cliBundled")
return cliBundled
}
override suspend fun retry() {
assertNotEdt("retry")
retries += 1
@@ -45,6 +45,9 @@ interface KiloAppRpcApi : RemoteApi<Unit> {
/** Core platform downloaded by the backend process. */
suspend fun cliPlatform(): String
/** Whether the running Core is bundled in the plugin (true) or downloaded (false). */
suspend fun cliBundled(): Boolean
/** Retry app connection or loading after a failure. */
suspend fun retry()