diff --git a/.changeset/jetbrains-settings-config-actions.md b/.changeset/jetbrains-settings-config-actions.md new file mode 100644 index 00000000000..cb86d06039d --- /dev/null +++ b/.changeset/jetbrains-settings-config-actions.md @@ -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. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt index 2cfefb8c997..208fcaad728 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt @@ -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 { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt index b3e083f0fb0..ecbe1f14f7d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt @@ -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) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt new file mode 100644 index 00000000000..2f3e2c848cb --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenConfigActions.kt @@ -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().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().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().globalConfigPath()) + } + + override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("Config Opened", mapOf("surface" to "tool_window", "scope" to "global")) + service().openGlobalConfig { ok -> + if (!ok) failed() + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSettingsAction.kt index c5e28f29d8a..64c41c00ab3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSettingsAction.kt @@ -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")) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index 99fc7aef8b5..31693e20190 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -41,6 +41,10 @@ class KiloWorkspaceService internal constructor( } private val workspaces = ConcurrentHashMap() + private val localConfig = ConcurrentHashMap() + + @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) + } + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt index 0f67cd97ac0..08f2f575193 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt @@ -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("ai.kilocode.client.session.SessionManager") + val WORKSPACE_KEY = DataKey.create("ai.kilocode.client.session.Workspace") } fun newSession() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 2a1a7e1f10d..97e0a5c6202 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -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 } } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index 4310d07140f..bddd4a543bc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -70,14 +70,27 @@ + + + + + + + + + + + @@ -91,14 +104,10 @@ - - - diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 0f67a78dacc..1891e237ccc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index d6e5b07c313..d9485c135f5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -32,6 +32,12 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { .use { it.readText() } assertTrue(xml.contains("")) + assertTrue(xml.contains("")) + assertTrue(xml.contains("")) + assertTrue(xml.contains("")) + assertTrue(xml.contains("")) + assertFalse(xml.contains("")) } private fun event(action: AnAction): AnActionEvent { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt index 827307f5be8..f5564e965d4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt @@ -27,8 +27,12 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { var modelsGate: CompletableDeferred? = null var fileMatches = emptyList() var openResult = true + var localConfigPath = "/test/.kilo/kilo.jsonc" + var globalConfigPath = "/config/kilo.jsonc" val fileCalls = mutableListOf>() val opened = mutableListOf() + val localConfigs = mutableListOf() + 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 + } } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt index 4fd04426ccb..a34dc4be180 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt @@ -47,4 +47,16 @@ interface KiloWorkspaceRpcApi : RemoteApi { /** 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 }