feat(kilo-jetbrains): add settings config file actions

This commit is contained in:
kirillk
2026-06-02 21:07:20 -04:00
parent 7e856a57c4
commit 138a58f80f
13 changed files with 253 additions and 9 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve the JetBrains tool window settings menu with config file shortcuts and separate CLI restart and reinstall actions.
@@ -6,6 +6,8 @@ import ai.kilocode.backend.app.KiloAppState
import ai.kilocode.backend.app.KiloBackendAppService
import ai.kilocode.backend.app.LoadError
import ai.kilocode.backend.cli.KiloCliDataParser
import ai.kilocode.backend.cli.KiloBackendCliManager
import ai.kilocode.backend.cli.KiloCliConfigPath
import ai.kilocode.backend.workspace.AgentData
import ai.kilocode.backend.workspace.AgentInfo
import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager
@@ -39,6 +41,7 @@ import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.coroutines.resume
@@ -53,6 +56,15 @@ import kotlin.coroutines.resume
class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
companion object {
private val LOG = KiloLog.create(KiloWorkspaceRpcApiImpl::class.java)
private const val SCHEMA = "https://app.kilo.ai/config.json"
private val MODERN = listOf("kilo.jsonc", "kilo.json")
private val LEGACY = listOf("opencode.jsonc", "opencode.json")
private val GLOBAL = MODERN + LEGACY + "config.json"
private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode")
private val CONFIG = """{
"${'$'}schema": "$SCHEMA"
}
"""
}
private val app: KiloBackendAppService get() = service()
@@ -171,6 +183,55 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
return true
}
override suspend fun localConfigPath(directory: String): String = withContext(Dispatchers.IO) {
localConfig(directory).toString()
}
override suspend fun globalConfigPath(): String = withContext(Dispatchers.IO) {
globalConfig().toString()
}
override suspend fun openLocalConfig(directory: String): Boolean = openConfig(withContext(Dispatchers.IO) {
localConfig(directory)
})
override suspend fun openGlobalConfig(): Boolean = openConfig(withContext(Dispatchers.IO) {
globalConfig()
})
private suspend fun openConfig(path: Path): Boolean {
val target = withContext(Dispatchers.IO) {
Files.createDirectories(path.parent)
if (!Files.exists(path)) Files.writeString(path, CONFIG, StandardCharsets.UTF_8)
path
}
val vf = LocalFileSystem.getInstance().refreshAndFindFileByPath(target.toString()) ?: return false
val project = project(target) ?: run {
LOG.warn("No project available to open config file: $target")
return false
}
navigate(project, vf)
return true
}
private fun localConfig(directory: String): Path {
val root = file(clean(directory) ?: directory)?.takeIf { it.isAbsolute } ?: Path.of(directory).normalize()
val dirs = LOCAL_DIRS.map { root.resolve(it) } + root
val found = dirs.asSequence()
.flatMap { dir -> (MODERN + LEGACY).asSequence().map { name -> dir.resolve(name) } }
.firstOrNull { Files.exists(it) }
return found ?: root.resolve(".kilo").resolve("kilo.jsonc")
}
private fun globalConfig(): Path {
val env = KiloBackendCliManager().buildEnv("config")
val root = KiloCliConfigPath.resolve(env).toPath().normalize()
return GLOBAL.asSequence()
.map { root.resolve(it) }
.firstOrNull { Files.exists(it) }
?: root.resolve("kilo.jsonc")
}
private fun clean(path: String): String? {
val raw = path.trim().takeIf { it.isNotBlank() } ?: return null
return try {
@@ -73,7 +73,6 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware {
val actions = listOfNotNull(
ActionManager.getInstance().getAction("Kilo.NewSession"),
ActionManager.getInstance().getAction("Kilo.History"),
ActionManager.getInstance().getAction("Kilo.ShowProfile"),
ActionManager.getInstance().getAction("Kilo.Settings"),
)
toolWindow.setTitleActions(actions)
@@ -0,0 +1,67 @@
package ai.kilocode.client.actions
import ai.kilocode.client.KiloNotifications
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.SessionManager
import ai.kilocode.client.telemetry.Telemetry
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAware
abstract class ConfigAction(
private val label: String,
text: String,
description: String,
) : AnAction(text, description, null), DumbAware {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
protected fun text(path: String?) = KiloBundle.message(label, path ?: "...")
protected fun failed() {
KiloNotifications.error(KiloBundle.message("action.Kilo.OpenConfig.failed"))
}
}
class OpenLocalConfigAction : ConfigAction(
label = "action.Kilo.OpenLocalConfig.text",
text = KiloBundle.message("action.Kilo.OpenLocalConfig.text", "..."),
description = KiloBundle.message("action.Kilo.OpenLocalConfig.description"),
) {
override fun update(e: AnActionEvent) {
val dir = directory(e)
e.presentation.isEnabled = dir != null
e.presentation.text = text(dir?.let { service<KiloWorkspaceService>().localConfigPath(it) })
}
override fun actionPerformed(e: AnActionEvent) {
val dir = directory(e) ?: return
Telemetry.send("Config Opened", mapOf("surface" to "tool_window", "scope" to "local"))
service<KiloWorkspaceService>().openLocalConfig(dir) { ok ->
if (!ok) failed()
}
}
private fun directory(e: AnActionEvent): String? {
return e.getData(SessionManager.WORKSPACE_KEY)?.directory ?: e.project?.basePath
}
}
class OpenGlobalConfigAction : ConfigAction(
label = "action.Kilo.OpenGlobalConfig.text",
text = KiloBundle.message("action.Kilo.OpenGlobalConfig.text", "..."),
description = KiloBundle.message("action.Kilo.OpenGlobalConfig.description"),
) {
override fun update(e: AnActionEvent) {
e.presentation.text = text(service<KiloWorkspaceService>().globalConfigPath())
}
override fun actionPerformed(e: AnActionEvent) {
Telemetry.send("Config Opened", mapOf("surface" to "tool_window", "scope" to "global"))
service<KiloWorkspaceService>().openGlobalConfig { ok ->
if (!ok) failed()
}
}
}
@@ -3,7 +3,6 @@ package ai.kilocode.client.actions
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.KiloSettingsConfigurable
import ai.kilocode.client.telemetry.Telemetry
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.Configurable
@@ -15,7 +14,7 @@ import java.util.function.Predicate
class OpenSettingsAction : DumbAwareAction(
KiloBundle.message("action.Kilo.OpenSettings.text"),
KiloBundle.message("action.Kilo.OpenSettings.description"),
AllIcons.General.GearPlain,
null,
) {
override fun actionPerformed(e: AnActionEvent) {
Telemetry.send("Settings Opened", mapOf("surface" to "tool_window"))
@@ -41,6 +41,10 @@ class KiloWorkspaceService internal constructor(
}
private val workspaces = ConcurrentHashMap<String, Workspace>()
private val localConfig = ConcurrentHashMap<String, String>()
@Volatile
private var globalConfig: String? = null
// ------ RPC helpers ------
@@ -129,4 +133,50 @@ class KiloWorkspaceService internal constructor(
false
}
}
fun localConfigPath(directory: String): String? {
cs.launch {
try {
localConfig[directory] = call { this.localConfigPath(directory) }
} catch (e: Exception) {
LOG.warn("local config lookup failed for directory=$directory", e)
}
}
return localConfig[directory]
}
fun globalConfigPath(): String? {
cs.launch {
try {
globalConfig = call { this.globalConfigPath() }
} catch (e: Exception) {
LOG.warn("global config lookup failed", e)
}
}
return globalConfig
}
fun openLocalConfig(directory: String, done: (Boolean) -> Unit) {
cs.launch {
val ok = try {
call { this.openLocalConfig(directory) }
} catch (e: Exception) {
LOG.warn("local config open failed for directory=$directory", e)
false
}
done(ok)
}
}
fun openGlobalConfig(done: (Boolean) -> Unit) {
cs.launch {
val ok = try {
call { this.openGlobalConfig() }
} catch (e: Exception) {
LOG.warn("global config open failed", e)
false
}
done(ok)
}
}
}
@@ -1,11 +1,13 @@
package ai.kilocode.client.session
import ai.kilocode.client.app.Workspace
import ai.kilocode.rpc.dto.SessionDto
import com.intellij.openapi.actionSystem.DataKey
interface SessionManager {
companion object {
val KEY = DataKey.create<SessionManager>("ai.kilocode.client.session.SessionManager")
val WORKSPACE_KEY = DataKey.create<Workspace>("ai.kilocode.client.session.Workspace")
}
fun newSession()
@@ -34,6 +34,7 @@ class SessionSidePanelManager(
val component: JPanel = object : JPanel(BorderLayout()), DataProvider {
override fun getData(dataId: String): Any? {
if (SessionManager.KEY.`is`(dataId)) return this@SessionSidePanelManager
if (SessionManager.WORKSPACE_KEY.`is`(dataId)) return root
return null
}
}
@@ -70,14 +70,27 @@
<action id="Kilo.OpenSettings"
class="ai.kilocode.client.actions.OpenSettingsAction"/>
<action id="Kilo.OpenLocalConfig"
class="ai.kilocode.client.actions.OpenLocalConfigAction"/>
<action id="Kilo.OpenGlobalConfig"
class="ai.kilocode.client.actions.OpenGlobalConfigAction"/>
<group id="Kilo.CliGroup" text="CLI" popup="true">
<reference ref="Kilo.Restart"/>
<reference ref="Kilo.Reinstall"/>
</group>
<group id="Kilo.OpenConfigGroup" text="Config Files" popup="true">
<reference ref="Kilo.OpenLocalConfig"/>
<reference ref="Kilo.OpenGlobalConfig"/>
</group>
<group id="Kilo.SettingsGroup">
<reference ref="Kilo.OpenSettings"/>
<separator/>
<reference ref="Kilo.OpenConfigGroup"/>
<separator/>
<reference ref="Kilo.CliGroup"/>
</group>
@@ -91,14 +104,10 @@
<action id="Kilo.History"
class="ai.kilocode.client.actions.HistoryAction"/>
<action id="Kilo.ShowProfile"
class="ai.kilocode.client.actions.ShowProfileAction"/>
<!-- Declarative toolbar group for the Kilo tool window title bar -->
<group id="Kilo.ToolWindowToolbar">
<reference ref="Kilo.NewSession"/>
<reference ref="Kilo.History"/>
<reference ref="Kilo.ShowProfile"/>
<reference ref="Kilo.Settings"/>
</group>
@@ -259,8 +259,15 @@ action.Kilo.StopSession.text=Stop Session
action.Kilo.StopSession.description=Stop the current Kilo session
action.Kilo.SettingsGroup.text=Settings
action.Kilo.SettingsGroup.description=Kilo Code settings
action.Kilo.OpenSettings.text=Open Settings
action.Kilo.OpenSettings.description=Open Kilo Code settings
action.Kilo.OpenSettings.text=Open Settings...
action.Kilo.OpenSettings.description=Open Kilo Code settings dialog
action.Kilo.OpenConfigGroup.text=Config Files
action.Kilo.OpenConfigGroup.description=Open Kilo config files
action.Kilo.OpenLocalConfig.text=Local {0}
action.Kilo.OpenLocalConfig.description=Open or create the local Kilo config file
action.Kilo.OpenGlobalConfig.text=Global {0}
action.Kilo.OpenGlobalConfig.description=Open or create the global Kilo config file
action.Kilo.OpenConfig.failed=Failed to open Kilo config file
action.Kilo.CliGroup.text=CLI
action.Kilo.CliGroup.description=Kilo CLI actions
action.Kilo.Restart.text=Restart Kilo
@@ -32,6 +32,12 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() {
.use { it.readText() }
assertTrue(xml.contains("<group id=\"Kilo.CliGroup\" text=\"CLI\" popup=\"true\">"))
assertTrue(xml.contains("<reference ref=\"Kilo.Restart\"/>"))
assertTrue(xml.contains("<reference ref=\"Kilo.Reinstall\"/>"))
assertTrue(xml.contains("<group id=\"Kilo.OpenConfigGroup\" text=\"Config Files\" popup=\"true\">"))
assertTrue(xml.contains("<reference ref=\"Kilo.OpenConfigGroup\"/>"))
assertFalse(xml.contains("<action id=\"Kilo.ShowProfile\""))
assertFalse(xml.contains("<reference ref=\"Kilo.ShowProfile\"/>"))
}
private fun event(action: AnAction): AnActionEvent {
@@ -27,8 +27,12 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
var modelsGate: CompletableDeferred<Unit>? = null
var fileMatches = emptyList<WorkspaceFileDto>()
var openResult = true
var localConfigPath = "/test/.kilo/kilo.jsonc"
var globalConfigPath = "/config/kilo.jsonc"
val fileCalls = mutableListOf<Pair<String, String>>()
val opened = mutableListOf<String>()
val localConfigs = mutableListOf<String>()
var globalConfigs = 0
override suspend fun resolveProjectDirectory(hint: String): String {
assertNotEdt("resolveProjectDirectory")
@@ -62,4 +66,26 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
opened.add(path)
return openResult
}
override suspend fun localConfigPath(directory: String): String {
assertNotEdt("localConfigPath")
return localConfigPath
}
override suspend fun globalConfigPath(): String {
assertNotEdt("globalConfigPath")
return globalConfigPath
}
override suspend fun openLocalConfig(directory: String): Boolean {
assertNotEdt("openLocalConfig")
localConfigs.add(directory)
return openResult
}
override suspend fun openGlobalConfig(): Boolean {
assertNotEdt("openGlobalConfig")
globalConfigs += 1
return openResult
}
}
@@ -47,4 +47,16 @@ interface KiloWorkspaceRpcApi : RemoteApi<Unit> {
/** Open an absolute backend file path in the IDE. */
suspend fun openFile(path: String): Boolean
/** Resolve the editable local config target path. */
suspend fun localConfigPath(directory: String): String
/** Resolve the editable global config target path. */
suspend fun globalConfigPath(): String
/** Open or create the local config file in the IDE. */
suspend fun openLocalConfig(directory: String): Boolean
/** Open or create the global config file in the IDE. */
suspend fun openGlobalConfig(): Boolean
}