From a9a9b78b97290e855cda3dd7118a429503802396 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 16 Jul 2026 15:08:17 -0400 Subject: [PATCH 1/9] feat(jetbrains): add skills settings page --- .changeset/jetbrains-skills-settings.md | 5 + .../backend/rpc/KiloWorkspaceRpcApiImpl.kt | 2 +- .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 23 ++ .../kilocode/backend/testing/MockCliServer.kt | 6 + .../client/app/KiloWorkspaceService.kt | 9 + .../agents/AgentBehaviorConfigurable.kt | 1 + .../settings/agents/SkillsConfigurable.kt | 306 +++++++++++++++++ .../client/settings/base/SettingsListModel.kt | 2 + .../settings/base/SettingsListRenderer.kt | 3 + .../client/settings/base/SettingsListView.kt | 2 +- .../resources/kilo.jetbrains.frontend.xml | 7 + .../resources/messages/KiloBundle.properties | 16 + .../agents/AgentBehaviorConfigurableTest.kt | 3 +- .../settings/agents/SkillsSettingsUiTest.kt | 320 ++++++++++++++++++ .../client/testing/FakeAgentBehaviorRpcApi.kt | 13 +- 15 files changed, 713 insertions(+), 5 deletions(-) create mode 100644 .changeset/jetbrains-skills-settings.md create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt diff --git a/.changeset/jetbrains-skills-settings.md b/.changeset/jetbrains-skills-settings.md new file mode 100644 index 0000000000..421979b1b8 --- /dev/null +++ b/.changeset/jetbrains-skills-settings.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Add a Skills settings page in JetBrains for viewing, opening, deleting, and configuring skill sources. 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 7d0311b6c6..3063f81a38 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 @@ -311,7 +311,7 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { } descriptor.navigate(true) if (cont.isActive) cont.resume(Unit) - }, ModalityState.any()) + }, ModalityState.current()) } private fun project(path: Path): Project? { diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index 4050343723..af12fd4439 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -78,6 +78,29 @@ class KiloAgentBehaviorRpcApiImplTest { assertContains(err.message.orEmpty(), "HTTP 400") } + @Test + fun `skills and remove skill call CLI endpoints`() = runBlocking { + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"/tmp/skill/SKILL.md"}, + {"name":"builtin","location":"builtin"} + ]""".trimIndent() + val rpc = rpc() + + val skills = rpc.skills("/test project") + assertEquals(listOf("plan", "builtin"), skills.map { it.name }) + assertEquals("Plan work", skills.single { it.name == "plan" }.description) + + assertTrue(rpc.removeSkill("/test project", "/tmp/skill/SKILL.md")) + assertEquals("{\"location\":\"/tmp/skill/SKILL.md\"}", mock.lastSkillRemoveBody) + assertEquals(1, mock.requestCount("/kilocode/skill/remove")) + + mock.skillRemoveStatus = 400 + val err = assertFailsWith { + rpc.removeSkill("/test", "/tmp/missing/SKILL.md") + } + assertContains(err.message.orEmpty(), "HTTP 400") + } + @Test fun `mcp config writes global and workspace patches`() = runBlocking { mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}""" diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 888b07e113..b0771572ff 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -67,9 +67,11 @@ class MockCliServer : AutoCloseable { @Volatile var mcpStatus = 200 @Volatile var mcpActionStatus = 200 @Volatile var agentRemoveStatus = 200 + @Volatile var skillRemoveStatus = 200 @Volatile var agentBuilderStatus = 200 @Volatile var lastMcpActionPath: String? = null @Volatile var lastAgentRemoveBody: String? = null + @Volatile var lastSkillRemoveBody: String? = null @Volatile var lastAgentBuilderPath: String? = null @Volatile var lastAgentBuilderBody: String? = null @Volatile var lastAgentBuilderMethod: String? = null @@ -368,6 +370,10 @@ class MockCliServer : AutoCloseable { lastAgentRemoveBody = body respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""") } + bare == "/kilocode/skill/remove" && method == "POST" -> { + lastSkillRemoveBody = body + respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""") + } bare == "/command" -> respond(output, commandsStatus, commands) bare == "/skill" -> respond(output, skillsStatus, skills) bare == "/mcp" -> respond(output, mcpStatus, mcp) 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 5836ae0cea..d443bf0147 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 @@ -168,6 +168,15 @@ class KiloWorkspaceService internal constructor( } } + suspend fun openFile(path: String, line: Int? = null, column: Int? = null): Boolean { + return try { + call { openFile(path, line, column) } + } catch (e: Exception) { + LOG.warn("workspace file open failed for path=$path", e) + false + } + } + suspend fun localConfigTarget(directory: String): ConfigTargetDto? { return try { val target = call { this.localConfigTarget(directory) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt index 800f9e26a8..e127e610da 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt @@ -25,6 +25,7 @@ class AgentBehaviorConfigurable : SearchableConfigurable { listOf( KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID, KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID, + KiloBundle.message("settings.agentBehavior.skills.displayName") to SkillsConfigurable.ID, ).forEach { (label, id) -> panel.next(ActionLink(label) { e -> val src = e.source as? JComponent ?: return@ActionLink diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt new file mode 100644 index 0000000000..ddfd190f6a --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -0,0 +1,306 @@ +package ai.kilocode.client.settings.agents + +import ai.kilocode.client.app.KiloAgentBehaviorService +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.SettingsBadge +import ai.kilocode.client.settings.base.SettingsListCell +import ai.kilocode.client.settings.base.SettingsListConfig +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsListPanel +import ai.kilocode.client.settings.base.SettingsListSelection +import ai.kilocode.client.settings.base.SettingsListView +import ai.kilocode.client.settings.base.SettingsMessageException +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.log.KiloLog +import ai.kilocode.rpc.dto.ConfigPatchDto +import ai.kilocode.rpc.dto.SkillsConfigDto +import ai.kilocode.rpc.dto.SkillsPatchDto +import ai.kilocode.rpc.dto.SkillDto +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.components.service +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.awt.BorderLayout +import javax.swing.JComponent +import javax.swing.ScrollPaneConstants + +private val edt = Dispatchers.EDT + ModalityState.any().asContextElement() + +class SkillsConfigurable : AgentBehaviorConfigurableBase() { + override fun getId(): String = ID + override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.skills.displayName") + override fun create(cs: CoroutineScope, dir: String): JComponent = SkillsSettingsUi(cs, dir) + override fun update(ui: JComponent, dir: String) { + (ui as? SkillsSettingsUi)?.setDirectory(dir) + } + override fun scrollReadyShell() = false + + companion object { const val ID = "ai.kilocode.jetbrains.settings.agentBehavior.skills" } +} + +internal class SkillsSettingsUi( + cs: CoroutineScope, + dir: String, + private val choose: (JComponent) -> String? = ::chooseSkillPath, + private val input: (String, String) -> String? = ::inputSkillUrl, +) : SettingsListPanel(cs, SettingsListConfig.Equal.copy(tooltip = false)) { + private var dir = dir + private var skills = emptyMap() + internal val sources = SkillSourcesView(this, choose, input) + + init { + start() + content.add(sources, BorderLayout.SOUTH) + } + + fun setDirectory(value: String) { + if (value == dir) return + dir = value + reload() + } + + override suspend fun fetch(): List { + val items = service().skills(dir) + val config = config() + withContext(edt) { + skills = items.associateBy { key(it) } + sources.refresh(config) + } + LOG.info("skills settings fetch dir=$dir total=${items.size}") + return items.map(::item) + } + + override fun afterApply() { + sources.refresh(config()) + } + + override fun onCell(key: String, cellId: String) { + val skill = skills[key] ?: return + when (cellId) { + OPEN_CELL -> open(skill) + DELETE_CELL -> remove(skill) + } + } + + override fun searchPlaceholder() = KiloBundle.message("settings.agentBehavior.skills.search") + + override fun emptyText() = KiloBundle.message("settings.agentBehavior.skills.empty") + + internal fun updateSources(paths: List, urls: List) { + mutateAndReload(SettingsListSelection.Preserve, KiloBundle.message("settings.agentBehavior.saving")) { + val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = paths, urls = urls)) + if (service().updateConfig(patch) == null) { + throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.save.failed")) + } + true + } + } + + private fun item(skill: SkillDto) = object : SettingsListItem { + override val key = key(skill) + override val title = skill.name + override val note = skill.location.takeUnless { builtin(it) } + override val description = skill.description + override val badges = listOf( + SettingsBadge(KiloBundle.message("settings.agentBehavior.badge.builtin"), UiStyle.Badge.Secondary), + ).takeIf { builtin(skill.location) } ?: emptyList() + override val cells = if (builtin(skill.location)) emptyList() else listOf( + SettingsListCell( + OPEN_CELL, + KiloBundle.message("settings.agentBehavior.skills.open"), + primary = true, + ), + SettingsListCell( + DELETE_CELL, + KiloBundle.message("common.delete"), + icon = AllIcons.Actions.GC, + iconOnly = true, + ), + ) + } + + private fun open(skill: SkillDto) { + launch("open") { id -> + service().openFile(skill.location) + finishOpen(id) + } + } + + private suspend fun finishOpen(id: Int) { + withContext(edt) { + if (!active(id)) return@withContext + setBusy(false) + clearProgress() + } + } + + private fun remove(skill: SkillDto) { + val result = Messages.showYesNoDialog( + KiloBundle.message("settings.agentBehavior.skills.delete.message", skill.name), + KiloBundle.message("settings.agentBehavior.skills.delete.title"), + KiloBundle.message("common.delete"), + Messages.getCancelButton(), + Messages.getQuestionIcon(), + ) + if (result != Messages.YES) return + mutateAndReload(selectionIndex()) { + if (!service().removeSkill(dir, skill.location)) { + throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.delete.failed")) + } + true + } + } + + private fun config() = service().state.value.config?.skills ?: SkillsConfigDto() + + private companion object { + const val OPEN_CELL = "open" + const val DELETE_CELL = "delete" + const val BUILTIN = "builtin" + const val LEGACY_BUILTIN = "" + val LOG = KiloLog.create(SkillsSettingsUi::class.java) + + fun key(skill: SkillDto) = skill.location.ifBlank { skill.name } + fun builtin(location: String) = location == BUILTIN || location == LEGACY_BUILTIN + } +} + +internal class SkillSourcesView( + private val parent: SkillsSettingsUi, + private val choose: (JComponent) -> String?, + private val input: (String, String) -> String?, +) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) { + private val view = SettingsListView( + KiloBundle.message("settings.agentBehavior.skills.sources.empty"), + SettingsListConfig.Preferred.copy(description = false), + ) { key, id -> + if (id == DELETE_CELL) remove(key) + } + private var cfg = SkillsConfigDto() + + internal fun sourceList() = view.list + + init { + border = JBUI.Borders.compound( + JBUI.Borders.customLineTop(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()), + JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0), + ) + next(toolbar()) + next(JBScrollPane(view).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + preferredSize = JBUI.size(0, JBUI.scale(160)) + maximumSize = JBUI.size(Int.MAX_VALUE, JBUI.scale(160)) + }) + } + + fun refresh(config: SkillsConfigDto) { + cfg = config + view.update(rows(config)) + } + + private fun toolbar(): JComponent { + val group = DefaultActionGroup(AddPathAction(), AddUrlAction()) + val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true) + toolbar.targetComponent = this + toolbar.updateActionsImmediately() + return toolbar.component + } + + internal fun addPath() { + val path = choose(parent)?.trim()?.takeIf { it.isNotBlank() } ?: return + if (path in cfg.paths) return + parent.updateSources(cfg.paths + path, cfg.urls) + } + + internal fun addUrl() { + val url = input( + KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.title"), + KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.prompt"), + )?.trim()?.takeIf { it.isNotBlank() } ?: return + if (url in cfg.urls) return + parent.updateSources(cfg.paths, cfg.urls + url) + } + + private fun rows(config: SkillsConfigDto): List { + val paths = config.paths.map { source(PATH_PREFIX, it, KiloBundle.message("settings.agentBehavior.skills.sources.paths")) } + val urls = config.urls.map { source(URL_PREFIX, it, KiloBundle.message("settings.agentBehavior.skills.sources.urls")) } + return paths + urls + } + + private fun source(prefix: String, value: String, section: String) = object : SettingsListItem { + override val key = prefix + value + override val title = value + override val section = section + override val cells = listOf(SettingsListCell( + DELETE_CELL, + KiloBundle.message("common.delete"), + icon = AllIcons.Actions.GC, + iconOnly = true, + )) + } + + private fun remove(key: String) { + when { + key.startsWith(PATH_PREFIX) -> parent.updateSources(cfg.paths - key.removePrefix(PATH_PREFIX), cfg.urls) + key.startsWith(URL_PREFIX) -> parent.updateSources(cfg.paths, cfg.urls - key.removePrefix(URL_PREFIX)) + } + } + + private inner class AddPathAction : DumbAwareAction( + KiloBundle.message("settings.agentBehavior.skills.sources.addPath"), + null, + AllIcons.General.Add, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun actionPerformed(e: AnActionEvent) = addPath() + } + + private inner class AddUrlAction : DumbAwareAction( + KiloBundle.message("settings.agentBehavior.skills.sources.addUrl"), + null, + AllIcons.General.Add, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun actionPerformed(e: AnActionEvent) = addUrl() + } + + private companion object { + const val DELETE_CELL = "delete" + const val PATH_PREFIX = "path:" + const val URL_PREFIX = "url:" + } +} + +private fun chooseSkillPath(parent: JComponent): String? { + val descriptor = FileChooserDescriptor(false, true, false, false, false, false).apply { + title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title") + description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt") + } + return FileChooser.chooseFile(descriptor, parent, null, null as VirtualFile?)?.path +} + +private fun inputSkillUrl(title: String, prompt: String): String? = Messages.showInputDialog( + prompt, + title, + Messages.getQuestionIcon(), +) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt index 81cac63adc..eac5325e2d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt @@ -21,6 +21,7 @@ internal data class SettingsListConfig( val height: SettingsListRowHeight, val description: Boolean = true, val descriptionIndent: Boolean = true, + val tooltip: Boolean = true, ) { companion object { val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL) @@ -41,6 +42,7 @@ internal data class SettingsListCell( internal interface SettingsListItem { val key: String val title: String + val note: String? get() = null val description: String? get() = null val icon: Icon? get() = null val section: String? get() = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt index 314e2c64b7..074ea866be 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt @@ -83,6 +83,9 @@ internal class SettingsListRenderer( title.clear() title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) + value.note?.takeIf { it.isNotBlank() }?.let { + title.append(" $it", SimpleTextAttributes.GRAYED_ATTRIBUTES) + } syncBadges(value) icon.icon = value.icon mark.isVisible = value.icon != null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt index 661ac90103..73eebcda40 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt @@ -24,7 +24,7 @@ internal class SettingsListView( private val model = CollectionListModel() internal val list = object : JBList(model) { override fun getToolTipText(event: MouseEvent): String? { - if (!cfg.description) return null + if (!cfg.description || !cfg.tooltip) return null val idx = locationToIndex(event.point) if (idx < 0) return null val bounds = getCellBounds(idx, idx) ?: 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 d08df1f9f9..02630c6f17 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 @@ -76,6 +76,13 @@ bundle="messages.KiloBundle" key="settings.agentBehavior.mcp.displayName"/> + + edt { panel.dispose(); true } } + ui = null + scope?.cancel() + scope = null + } finally { + super.tearDown() + } + } + + fun `test loads skills with location note and builtins have no actions`() { + val panel = panel() + + flushUntil { rows(panel).size == 2 } + + edt { + val rows = rows(panel) + val custom = rows.single { it.key == CUSTOM } + assertEquals("plan", custom.title) + assertEquals(CUSTOM, custom.note) + assertEquals("Plan work", custom.description) + assertEquals(listOf("open", "delete"), custom.cells.map { it.id }) + val open = custom.cells.single { it.id == "open" } + assertEquals(KiloBundle.message("settings.agentBehavior.skills.open"), open.label) + assertTrue(open.primary) + assertFalse(open.iconOnly) + assertNull(open.icon) + assertTrue(custom.cells.single { it.id == "delete" }.iconOnly) + val builtin = rows.single { it.key == "builtin" } + assertEquals("thinking", builtin.title) + assertNull(builtin.note) + assertEquals(listOf("built-in"), builtin.badges.map { it.text }) + assertTrue(builtin.cells.isEmpty()) + assertEquals(listOf(DIR), agentRpc.skillCalls) + true + } + } + + fun `test skills list does not show description tooltips`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + + edt { + val list = skillsList(panel) + list.size = Dimension(520, 320) + list.doLayout() + val bounds = list.getCellBounds(0, 0) + + assertNull(list.getToolTipText(mouse(list, MouseEvent.MOUSE_MOVED, Point(bounds.x + 8, bounds.y + 8)))) + true + } + } + + fun `test renderer puts location on first line and description on preview line`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + + edt { + val list = skillsList(panel) + val row = rows(panel).single { it.key == CUSTOM } + val idx = rows(panel).indexOf(row) + val comp = list.cellRenderer.getListCellRendererComponent(list, row, idx, true, true) + comp.setSize(520, list.fixedCellHeight) + layout(comp) + val title = components(comp).filterIsInstance().single() + val labels = components(comp).filterIsInstance().filter { it.isVisible }.map { it.text } + + assertEquals("plan $CUSTOM", title.toString()) + assertTrue(labels.contains("Plan work")) + true + } + } + + fun `test open action calls direct open file`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + + click(skillsList(panel), panel, CUSTOM, "open") + + flushUntil { workspaceRpc.opened.contains(CUSTOM) } + assertEquals(listOf(CUSTOM), workspaceRpc.opened) + assertTrue(workspaceRpc.fileCalls.isEmpty()) + } + + fun `test delete action removes skill and reloads`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + TestDialogManager.setTestDialog(TestDialog.YES) + + click(skillsList(panel), panel, CUSTOM, "delete") + + flushUntil { rows(panel).none { it.key == CUSTOM } } + assertEquals(listOf(DIR to CUSTOM), agentRpc.skillRemovals) + } + + fun `test delete action requires confirmation`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + TestDialogManager.setTestDialog { Messages.NO } + + click(skillsList(panel), panel, CUSTOM, "delete") + + edt { UIUtil.dispatchAllInvocationEvents(); true } + assertTrue(agentRpc.skillRemovals.isEmpty()) + assertTrue(edt { rows(panel).any { it.key == CUSTOM } }) + } + + fun `test add path and url write skills config patch`() { + var path = "/extra/skills" + var url = "https://skills.test/index.json" + val panel = panel(choose = { path }, input = { _, _ -> url }) + flushUntil { rows(panel).size == 2 } + + edt { panel.sources.addPath(); true } + flushUntil { appRpc.configPatches.size == 1 } + flushUntil { edt { skillsList(panel).isEnabled } } + edt { panel.sources.addUrl(); true } + flushUntil { appRpc.configPatches.size == 2 } + + val paths = appRpc.configPatches.first().skills!!.paths + val urls = appRpc.configPatches.last().skills!!.urls + assertEquals(listOf("/global/skills", path), paths) + assertEquals(listOf("https://skills.test/base.json", url), urls) + } + + fun `test delete source writes skills config patch`() { + val panel = panel() + flushUntil { rows(panel).size == 2 && sourceRows(panel).size == 2 } + + click(sourceList(panel), panel, "path:/global/skills", "delete") + + flushUntil { appRpc.configPatches.size == 1 } + val patch = appRpc.configPatches.single().skills!! + assertEquals(emptyList(), patch.paths) + assertEquals(listOf("https://skills.test/base.json"), patch.urls) + } + + fun `test search filters skills by name`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + + edt { + components(panel).filterIsInstance().single().text = "think" + UIUtil.dispatchAllInvocationEvents() + true + } + + flushUntil { rows(panel).map { it.key } == listOf("builtin") } + } + + private fun panel( + choose: (JComponent) -> String? = { null }, + input: (String, String) -> String? = { _, _ -> null }, + ): SkillsSettingsUi { + install() + val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, input) } + ui = panel + edt { panel.reload(); true } + return panel + } + + private fun install() { + val cs = CoroutineScope(SupervisorJob()) + scope = cs + appRpc = FakeAppRpcApi() + agentRpc = FakeAgentBehaviorRpcApi().apply { + skills = listOf( + SkillDto("plan", "Plan work", CUSTOM), + SkillDto("thinking", "Built in", "builtin"), + ) + } + workspaceRpc = FakeWorkspaceRpcApi() + app = KiloAppService(cs, appRpc) + val ready = KiloAppStateDto( + KiloAppStatusDto.READY, + config = ConfigDto(skills = SkillsConfigDto( + paths = listOf("/global/skills"), + urls = listOf("https://skills.test/base.json"), + )), + ) + app._state.value = ready + appRpc.state.value = ready + ApplicationManager.getApplication().replaceService(KiloAppService::class.java, app, testRootDisposable) + ApplicationManager.getApplication().replaceService(KiloAgentBehaviorService::class.java, KiloAgentBehaviorService(cs, agentRpc), testRootDisposable) + ApplicationManager.getApplication().replaceService(KiloWorkspaceService::class.java, KiloWorkspaceService(cs, workspaceRpc), testRootDisposable) + } + + private fun click(list: JBList, panel: SkillsSettingsUi, key: String, id: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val rows = if (list === skillsList(panel)) rows(panel) else sourceRows(panel) + val idx = rows.indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = settingsListCellBounds(list, idx, selected = true).getValue(id) + click(list, center(area)) + true + } + } + + private fun rows(panel: SkillsSettingsUi): List = items(skillsList(panel)) + + private fun sourceRows(panel: SkillsSettingsUi): List = items(sourceList(panel)) + + private fun items(list: JBList): List { + val model = list.model + return (0 until model.size).map { model.getElementAt(it) } + } + + private fun skillsList(panel: SkillsSettingsUi) = components(panel).filterIsInstance>().first() + + private fun sourceList(panel: SkillsSettingsUi) = components(panel).filterIsInstance>().last() + + private fun components(root: java.awt.Component): List { + val out = mutableListOf() + fun visit(item: java.awt.Component) { + out += item + if (item is Container) item.components.forEach { visit(it) } + } + visit(root) + return out + } + + private fun layout(root: java.awt.Component) { + root.doLayout() + if (root is Container) root.components.filterIsInstance().forEach { layout(it) } + UIUtil.dispatchAllInvocationEvents() + } + + private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2) + + private fun click(list: JBList, point: Point) { + fire(list, mouse(list, MouseEvent.MOUSE_PRESSED, point)) + fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point)) + } + + private fun mouse(list: JBList, id: Int, point: Point) = MouseEvent( + list, + id, + System.currentTimeMillis(), + if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + + private fun edt(block: () -> T): T { + var result: T? = null + ApplicationManager.getApplication().invokeAndWait { result = block() } + @Suppress("UNCHECKED_CAST") + return result as T + } + + private fun flushUntil(done: () -> Boolean) = runBlocking { + repeat(300) { + delay(10) + edt { UIUtil.dispatchAllInvocationEvents(); true } + if (done()) return@runBlocking + } + edt { UIUtil.dispatchAllInvocationEvents(); true } + assertTrue(done()) + } + + private companion object { + const val DIR = "/test" + const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md" + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt index bbcc487317..bb11472b06 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt @@ -11,9 +11,12 @@ import ai.kilocode.rpc.dto.SkillDto class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var agents = emptyList() + var skills = emptyList() var mcps = emptyList() var mcpConfigs = emptyMap() val agentCalls = mutableListOf() + val skillCalls = mutableListOf() + val skillRemovals = mutableListOf>() val mcpCalls = mutableListOf() val mcpConfigCalls = mutableListOf() val mcpSaves = mutableListOf>() @@ -28,9 +31,11 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var afterMcpConnect: (suspend (String, String) -> Unit)? = null var createError: Exception? = null var removeError: Exception? = null + var removeSkillError: Exception? = null var mcpStatusError: Exception? = null var mcpConnectError: Exception? = null var removeResult = true + var removeSkillResult = true var mcpConnectResult = true var mcpDisconnectResult = true var mcpAuthenticateResult = true @@ -43,12 +48,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { override suspend fun skills(directory: String): List { assertNotEdt("agentBehavior.skills") - return emptyList() + skillCalls.add(directory) + return skills } override suspend fun removeSkill(directory: String, location: String): Boolean { assertNotEdt("agentBehavior.removeSkill") - return false + removeSkillError?.let { throw it } + skillRemovals.add(directory to location) + if (removeSkillResult) skills = skills.filterNot { it.location == location } + return removeSkillResult } override suspend fun removeAgent(directory: String, name: String): Boolean { From 61d26e666493ae04b4dc0526311b315e67a6c4c0 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 11:28:45 -0400 Subject: [PATCH 2/9] feat(jetbrains): improve skills settings --- .changeset/jetbrains-skills-settings.md | 2 +- .../backend/app/KiloBackendAppService.kt | 8 + .../backend/app/KiloBackendSessionManager.kt | 4 + .../kilocode/backend/cli/KiloCliDataParser.kt | 2 +- .../rpc/KiloAgentBehaviorRpcApiImpl.kt | 94 ++++- .../backend/rpc/KiloWorkspaceDtoMapper.kt | 1 + .../backend/rpc/KiloWorkspaceRpcApiImpl.kt | 2 +- .../backend/workspace/KiloBackendWorkspace.kt | 1 + .../backend/workspace/KiloWorkspaceState.kt | 1 + .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 60 ++- .../kilocode/backend/testing/MockCliServer.kt | 5 + .../ai/kilocode/client/KiloNotifications.kt | 9 + .../client/app/KiloAgentBehaviorService.kt | 7 +- .../settings/agents/SkillsConfigurable.kt | 386 +++++++++++++++--- .../client/settings/base/SettingsListModel.kt | 3 + .../client/settings/base/SettingsListView.kt | 54 ++- .../client/settings/base/SettingsPanel.kt | 8 + .../resources/messages/KiloBundle.properties | 16 +- .../settings/agents/SkillsSettingsUiTest.kt | 278 +++++++++++-- .../settings/base/SettingsListViewTest.kt | 13 + .../client/testing/FakeAgentBehaviorRpcApi.kt | 21 + .../kilocode/rpc/KiloAgentBehaviorRpcApi.kt | 4 + .../kotlin/ai/kilocode/rpc/dto/SkillDto.kt | 1 + 23 files changed, 871 insertions(+), 109 deletions(-) diff --git a/.changeset/jetbrains-skills-settings.md b/.changeset/jetbrains-skills-settings.md index 421979b1b8..128e35207d 100644 --- a/.changeset/jetbrains-skills-settings.md +++ b/.changeset/jetbrains-skills-settings.md @@ -2,4 +2,4 @@ "@kilocode/kilo-jetbrains": minor --- -Add a Skills settings page in JetBrains for viewing, opening, deleting, and configuring skill sources. +Support viewing, opening, editing, deleting, and configuring JetBrains skill sources. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 5bbfae2186..0318e49d11 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -820,11 +820,13 @@ class KiloBackendAppService private constructor( } } "global.disposed" -> { + logSessionDisposalRisk("global.disposed") log.info("SSE global.disposed — triggering full application reload") val current = _appState.value if (current is KiloAppState.Ready) load() } "server.instance.disposed" -> { + logSessionDisposalRisk("server.instance.disposed") log.info("SSE server.instance.disposed — triggering full application reload") val current = _appState.value if (current is KiloAppState.Ready) load() @@ -835,6 +837,12 @@ class KiloBackendAppService private constructor( } } + private fun logSessionDisposalRisk(event: String) { + val active = sessions.statuses.value.filterValues { it.type != "idle" } + if (active.isEmpty()) return + log.warn("SSE $event while sessions are active; sessions may be cancelled count=${active.size} statuses=${active.values.map { it.type }.distinct()}") + } + private suspend fun clear() { synchronized(loadLock) { val jobs = listOfNotNull(loader, eventWatcher) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index f5b9fc4f71..f48dbf9038 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -86,6 +86,10 @@ class KiloBackendSessionManager( } fun stop() { + val active = _statuses.value.filterValues { it.type != "idle" } + if (active.isNotEmpty()) { + log.warn("Session manager stopping with active sessions count=${active.size} statuses=${active.values.map { it.type }.distinct()}") + } watcher?.cancel() watcher = null client = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index fee794849b..868a1c6306 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -625,7 +625,7 @@ object KiloCliDataParser { val obj = item.obj() ?: return@mapNotNull null val name = obj.str("name") ?: return@mapNotNull null val location = obj.str("location") ?: return@mapNotNull null - SkillDto(name = name, description = obj.str("description"), location = location) + SkillDto(name = name, description = obj.str("description"), location = location, content = obj.str("content")) } fun parseAgentBehaviorCommands(raw: String): List = diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index d728a6b6aa..9d3e505608 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -14,6 +14,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto import ai.kilocode.rpc.dto.PermissionRuleItemDto +import ai.kilocode.rpc.dto.SkillDto import com.intellij.openapi.components.service import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -23,6 +24,9 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.net.URLEncoder +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path import java.nio.charset.StandardCharsets import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger @@ -33,6 +37,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = private val JSON = "application/json".toMediaType() private val saved = ConcurrentHashMap() private val port = AtomicInteger(-1) + private val extensions = setOf("md", "markdown", "txt", "text", "html", "htm") } private val app: KiloBackendAppService get() = backend ?: service() @@ -56,11 +61,52 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = } } - override suspend fun skills(directory: String) = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + override suspend fun skills(directory: String): List { + val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + return items.map { item -> item.copy(content = skillContent(item) ?: item.content) } + } override suspend fun removeSkill(directory: String, location: String): Boolean = post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location)))) + override suspend fun reloadSkills(directory: String): Boolean { + LOG.info("Skills reload requested dir=$directory") + if (hasActiveSession(directory)) { + LOG.warn("Skills reload blocked by active session dir=$directory") + return false + } + runCatching { post(directory, "/instance/reload") }.onFailure { err -> + LOG.warn("Skills reload failed dir=$directory", err) + }.getOrThrow() + LOG.info("Skills reload succeeded dir=$directory") + return true + } + + override suspend fun saveSkill(directory: String, location: String, content: String): Boolean { + LOG.info("Skill save requested dir=$directory location=$location") + app.requireReady() + val raw = normalizeWorkspacePath(location) ?: run { + LOG.warn("Skill save rejected: invalid location dir=$directory location=$location") + return false + } + val path = try { + Path.of(raw).normalize() + } catch (err: InvalidPathException) { + LOG.warn("Skill save rejected: invalid path dir=$directory location=$location", err) + return false + } + if (!path.isAbsolute || !isSkillFile(path)) { + LOG.warn("Skill save rejected: not a skill file dir=$directory path=$path") + return false + } + withContext(Dispatchers.IO) { + Files.writeString(path, content, StandardCharsets.UTF_8) + } + LOG.info("Skill file saved dir=$directory path=$path bytes=${content.toByteArray(StandardCharsets.UTF_8).size}") + LOG.info("Skill save reload deferred dir=$directory path=$path") + return true + } + override suspend fun removeAgent(directory: String, name: String): Boolean = post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name)))) @@ -139,6 +185,46 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return true } + private fun hasActiveSession(directory: String): Boolean { + val active = app.sessions.statuses.value.filterValues { it.type != "idle" } + if (active.isNotEmpty()) { + LOG.info("Skills reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}") + return true + } + val permissions = runCatching { app.chat.pendingPermissions(directory) }.onFailure { err -> + LOG.warn("Skills reload pending permission check failed dir=$directory", err) + }.getOrDefault(emptyList()) + if (permissions.isNotEmpty()) { + LOG.info("Skills reload pending permissions dir=$directory count=${permissions.size}") + return true + } + val questions = runCatching { app.chat.pendingQuestions(directory) }.onFailure { err -> + LOG.warn("Skills reload pending question check failed dir=$directory", err) + }.getOrDefault(emptyList()) + if (questions.isNotEmpty()) { + LOG.info("Skills reload pending questions dir=$directory count=${questions.size}") + return true + } + return false + } + + private suspend fun skillContent(skill: SkillDto): String? { + val raw = normalizeWorkspacePath(skill.location) ?: return null + val path = try { + Path.of(raw).normalize() + } catch (_: InvalidPathException) { + return null + } + if (!path.isAbsolute || !isSkillFile(path)) return null + return runCatching { + withContext(Dispatchers.IO) { + if (!Files.isRegularFile(path)) null else Files.readString(path, StandardCharsets.UTF_8) + } + }.onFailure { err -> + LOG.warn("Skill content read failed: $path", err) + }.getOrNull() + } + private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) { val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable") val url = "http://127.0.0.1:${app.port}$path" @@ -234,6 +320,12 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = private fun encodePath(value: String): String = encode(value).replace("+", "%20") + private fun isSkillFile(path: Path): Boolean { + val name = path.fileName?.toString() ?: return false + if (name == "SKILL.md") return true + return name.substringAfterLast('.', "").lowercase() in extensions + } + private data class SavedMcp( val directory: String, val name: String, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index 8c61e1e4db..395ef9b4de 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -64,6 +64,7 @@ internal object KiloWorkspaceDtoMapper { name = s.name, description = s.description, location = s.location, + content = s.content, ) private fun provider(p: ProviderInfo) = ProviderDto( 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 3063f81a38..96e07f8f54 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 @@ -311,7 +311,7 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { } descriptor.navigate(true) if (cont.isActive) cont.resume(Unit) - }, ModalityState.current()) + }, ModalityState.nonModal()) } private fun project(path: Path): Project? { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt index b948b4822d..27e19d1f06 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt @@ -254,6 +254,7 @@ class KiloBackendWorkspace( name = s.name, description = s.description, location = s.location, + content = s.content, ) }) } catch (e: CancellationException) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index 0308089c27..37c58d853d 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -143,4 +143,5 @@ data class SkillInfo( val name: String, val description: String?, val location: String, + val content: String?, ) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index af12fd4439..eaebbb1089 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import java.nio.file.Files import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertContains @@ -80,8 +81,18 @@ class KiloAgentBehaviorRpcApiImplTest { @Test fun `skills and remove skill call CLI endpoints`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + val content = """--- + |name: plan + |description: Plan work + |--- + | + |# Fresh Plan + """.trimMargin() + Files.writeString(file, content) mock.skills = """[ - {"name":"plan","description":"Plan work","location":"/tmp/skill/SKILL.md"}, + {"name":"plan","description":"Plan work","location":"$file","content":"# Stale Plan"}, {"name":"builtin","location":"builtin"} ]""".trimIndent() val rpc = rpc() @@ -89,9 +100,10 @@ class KiloAgentBehaviorRpcApiImplTest { val skills = rpc.skills("/test project") assertEquals(listOf("plan", "builtin"), skills.map { it.name }) assertEquals("Plan work", skills.single { it.name == "plan" }.description) + assertEquals(content, skills.single { it.name == "plan" }.content) - assertTrue(rpc.removeSkill("/test project", "/tmp/skill/SKILL.md")) - assertEquals("{\"location\":\"/tmp/skill/SKILL.md\"}", mock.lastSkillRemoveBody) + assertTrue(rpc.removeSkill("/test project", file.toString())) + assertEquals("{\"location\":\"$file\"}", mock.lastSkillRemoveBody) assertEquals(1, mock.requestCount("/kilocode/skill/remove")) mock.skillRemoveStatus = 400 @@ -99,6 +111,48 @@ class KiloAgentBehaviorRpcApiImplTest { rpc.removeSkill("/test", "/tmp/missing/SKILL.md") } assertContains(err.message.orEmpty(), "HTTP 400") + + assertTrue(rpc.reloadSkills("/test project")) + assertEquals(1, mock.requestCount("/instance/reload")) + } + + @Test + fun `save skill supports configured markdown text and html files without reload`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = dir.resolve("test.md") + Files.writeString(file, "old") + val rpc = rpc() + + assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) + assertEquals("new content", Files.readString(file)) + assertEquals(0, mock.requestCount("/instance/reload")) + } + + @Test + fun `save skill writes content without reloading instance`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + Files.writeString(file, "old") + val rpc = rpc() + + assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) + + assertEquals("new content", Files.readString(file)) + assertEquals(0, mock.requestCount("/instance/reload")) + assertFalse(rpc.saveSkill("/test project", "builtin", "nope")) + } + + @Test + fun `reload skills is blocked by pending permissions`() = runBlocking { + mock.pendingPermissions = """[ + {"id":"per_test","sessionID":"ses_test","permission":"bash","patterns":["*"],"metadata":{}} + ]""".trimIndent() + val rpc = rpc() + + assertFalse(rpc.reloadSkills("/test project")) + + assertEquals(1, mock.requestCount("/permission")) + assertEquals(0, mock.requestCount("/instance/reload")) } @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index b0771572ff..fba97760a9 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -130,6 +130,8 @@ class MockCliServer : AutoCloseable { @Volatile var lastSessionRenamePath: String? = null @Volatile var lastSessionRenameBody: String? = null @Volatile var lastSessionRenameMethod: String? = null + @Volatile var pendingPermissions = "[]" + @Volatile var pendingQuestions = "[]" /** Configurable delay for all endpoint responses (ms). 0 = no delay. */ @Volatile var responseDelay: Long = 0 @@ -374,6 +376,7 @@ class MockCliServer : AutoCloseable { lastSkillRemoveBody = body respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""") } + bare == "/instance/reload" && method == "POST" -> respond(output, 200, "true") bare == "/command" -> respond(output, commandsStatus, commands) bare == "/skill" -> respond(output, skillsStatus, skills) bare == "/mcp" -> respond(output, mcpStatus, mcp) @@ -399,6 +402,8 @@ class MockCliServer : AutoCloseable { respond(output, cloudSessionImportStatus, cloudSessionImport) } bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) + bare == "/permission" && method == "GET" -> respond(output, 200, pendingPermissions) + bare == "/question" && method == "GET" -> respond(output, 200, pendingQuestions) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" -> diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt index d2bc692d61..570c73363f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt @@ -21,4 +21,13 @@ object KiloNotifications { ?: Notification(GROUP, title, content ?: "", NotificationType.ERROR) notification.notify(project) } + + fun info(title: String, content: String? = null) { + val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault } + val notification = NotificationGroupManager.getInstance() + .getNotificationGroup(GROUP) + ?.createNotification(title, content ?: "", NotificationType.INFORMATION) + ?: Notification(GROUP, title, content ?: "", NotificationType.INFORMATION) + notification.notify(project) + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt index 63f55818d3..1afdffea42 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt @@ -33,7 +33,7 @@ class KiloAgentBehaviorService internal constructor( suspend fun agents(directory: String): List = safe(emptyList()) { call { agents(directory) } } - suspend fun skills(directory: String): List = safe(emptyList()) { call { skills(directory) } } + suspend fun skills(directory: String): List = call { skills(directory) } suspend fun commands(directory: String): List = safe(emptyList()) { call { commands(directory) } } @@ -52,6 +52,11 @@ class KiloAgentBehaviorService internal constructor( suspend fun removeSkill(directory: String, location: String): Boolean = safe(false) { call { removeSkill(directory, location) } } + suspend fun reloadSkills(directory: String): Boolean = safe(false) { call { reloadSkills(directory) } } + + suspend fun saveSkill(directory: String, location: String, content: String): Boolean = + safe(false) { call { saveSkill(directory, location, content) } } + suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } } suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index ddfd190f6a..2caae3017b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -3,8 +3,12 @@ package ai.kilocode.client.settings.agents import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.KiloNotifications import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.settings.base.SettingsBadge +import ai.kilocode.client.settings.base.SettingsDraftPage +import ai.kilocode.client.settings.base.SettingsDraftState import ai.kilocode.client.settings.base.SettingsListCell import ai.kilocode.client.settings.base.SettingsListConfig import ai.kilocode.client.settings.base.SettingsListItem @@ -19,29 +23,47 @@ import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.SkillsConfigDto import ai.kilocode.rpc.dto.SkillsPatchDto import ai.kilocode.rpc.dto.SkillDto +import com.intellij.CommonBundle import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.components.service -import com.intellij.openapi.fileChooser.FileChooser -import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.EditorTextField +import com.intellij.ui.TitledSeparator import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextField import com.intellij.util.ui.JBUI import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import java.awt.BorderLayout +import javax.swing.JButton import javax.swing.JComponent +import javax.swing.JPanel import javax.swing.ScrollPaneConstants +import javax.swing.ListSelectionModel private val edt = Dispatchers.EDT + ModalityState.any().asContextElement() @@ -58,17 +80,27 @@ class SkillsConfigurable : AgentBehaviorConfigurableBase() { } internal class SkillsSettingsUi( - cs: CoroutineScope, + scope: CoroutineScope, dir: String, private val choose: (JComponent) -> String? = ::chooseSkillPath, private val input: (String, String) -> String? = ::inputSkillUrl, -) : SettingsListPanel(cs, SettingsListConfig.Equal.copy(tooltip = false)) { + private val edit: (SkillDto, Boolean) -> SkillEditDialogHandle = ::SkillEditDialog, +) : SettingsListPanel(scope, SettingsListConfig.Equal.copy(tooltip = false)), SettingsDraftPage { + private val cs = scope private var dir = dir private var skills = emptyMap() + private val app get() = service() + private val state = SettingsDraftState(skillsDraft(app.state.value.config?.skills ?: SkillsConfigDto()), ::saved) + private var draft: SkillsDraft + get() = state.draft + set(value) { + state.draft = value + } internal val sources = SkillSourcesView(this, choose, input) init { start() + setCenter(skillScroll()) content.add(sources, BorderLayout.SOUTH) } @@ -79,24 +111,30 @@ internal class SkillsSettingsUi( } override suspend fun fetch(): List { - val items = service().skills(dir) - val config = config() + val items = withTimeoutOrNull(SKILL_LOAD_TIMEOUT_MS) { + service().skills(dir) + } ?: throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.load.timeout")) withContext(edt) { + val dirty = state.modified() + val edit = draft + state.accept(skillsDraft(config())) + if (dirty) draft = state.draft.copy(edited = edit.edited, deleted = edit.deleted) skills = items.associateBy { key(it) } - sources.refresh(config) + sources.refresh(draft.sources) } LOG.info("skills settings fetch dir=$dir total=${items.size}") - return items.map(::item) + return rows(items) } override fun afterApply() { - sources.refresh(config()) + sources.refresh(draft.sources) } override fun onCell(key: String, cellId: String) { val skill = skills[key] ?: return when (cellId) { OPEN_CELL -> open(skill) + EDIT_CELL -> edit(skill) DELETE_CELL -> remove(skill) } } @@ -106,13 +144,81 @@ internal class SkillsSettingsUi( override fun emptyText() = KiloBundle.message("settings.agentBehavior.skills.empty") internal fun updateSources(paths: List, urls: List) { - mutateAndReload(SettingsListSelection.Preserve, KiloBundle.message("settings.agentBehavior.saving")) { - val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = paths, urls = urls)) - if (service().updateConfig(patch) == null) { - throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.save.failed")) + state.update { copy(sources = SkillsConfigDto(paths = paths, urls = urls)) } + sources.refresh(draft.sources) + } + + override fun modified(): Boolean = state.modified() + + override fun resetDraft() { + state.reset() + sources.refresh(draft.sources) + view.update(rows()) + clearProgress() + } + + override fun applyDraft() { + val token = state.start() ?: return + if (!launch("apply") { id -> + val target = token.target + var failed: String? = null + val behavior = service() + LOG.info("skills settings apply start dir=$dir edited=${target.edited.size} deleted=${target.deleted.size} paths=${target.sources.paths.size} urls=${target.sources.urls.size}") + for ((location, content) in target.edited) { + if (!behavior.saveSkill(dir, location, content)) { + failed = KiloBundle.message("settings.agentBehavior.save.failed") + break + } } - true - } + if (failed == null) { + for (location in target.deleted) { + if (!behavior.removeSkill(dir, location)) { + failed = KiloBundle.message("settings.agentBehavior.skills.delete.failed") + break + } + } + } + if (failed == null && target.sources != token.previous.sources) { + val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = target.sources.paths, urls = target.sources.urls)) + if (app.updateConfig(patch) == null) failed = KiloBundle.message("settings.agentBehavior.save.failed") + } + val items = behavior.skills(dir) + withContext(edt) { + if (!active(id)) { + if (failed == null) KiloNotifications.info(KiloBundle.message("settings.agentBehavior.skills.saved.notification")) + else KiloNotifications.error(failed) + return@withContext + } + if (failed == null) { + skills = items.associateBy { key(it) } + val next = skillsDraft(config()) + state.complete(token, next) + sources.refresh(draft.sources) + view.update(rows(items)) + clearProgress() + LOG.info("skills settings apply succeeded dir=$dir") + } else { + state.fail(token, failed) + sources.refresh(draft.sources) + view.update(rows(items)) + showError(failed) + LOG.warn("skills settings apply failed dir=$dir message=$failed") + } + setBusy(false) + } + }) return + showProgress(KiloBundle.message("settings.agentBehavior.saving")) + } + + private fun skillScroll() = JBScrollPane(view).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + } + + private fun rows(items: List = skills.values.toList()): List = items.mapNotNull { skill -> + if (skill.location in draft.deleted) return@mapNotNull null + item(skill) } private fun item(skill: SkillDto) = object : SettingsListItem { @@ -120,36 +226,45 @@ internal class SkillsSettingsUi( override val title = skill.name override val note = skill.location.takeUnless { builtin(it) } override val description = skill.description + override val doubleClick = EDIT_CELL override val badges = listOf( SettingsBadge(KiloBundle.message("settings.agentBehavior.badge.builtin"), UiStyle.Badge.Secondary), ).takeIf { builtin(skill.location) } ?: emptyList() - override val cells = if (builtin(skill.location)) emptyList() else listOf( + override val cells = listOfNotNull( SettingsListCell( OPEN_CELL, - KiloBundle.message("settings.agentBehavior.skills.open"), + KiloBundle.message("settings.agentBehavior.skills.openInEditor"), primary = true, + ).takeUnless { builtin(skill.location) }, + SettingsListCell( + EDIT_CELL, + KiloBundle.message("settings.agentBehavior.edit"), + primary = builtin(skill.location), ), SettingsListCell( DELETE_CELL, KiloBundle.message("common.delete"), icon = AllIcons.Actions.GC, iconOnly = true, - ), + ).takeUnless { builtin(skill.location) }, ) } - private fun open(skill: SkillDto) { - launch("open") { id -> - service().openFile(skill.location) - finishOpen(id) - } + private fun edit(skill: SkillDto) { + val current = skill.copy(content = content(skill)) + val dialog = edit(current, !builtin(skill.location)) + if (!dialog.showAndGet()) return + state.update { copy(edited = edited + (skill.location to dialog.content())) } + view.update(rows(), SettingsListSelection.Key(key(skill))) } - private suspend fun finishOpen(id: Int) { - withContext(edt) { - if (!active(id)) return@withContext - setBusy(false) - clearProgress() + private fun open(skill: SkillDto) { + if (builtin(skill.location)) return + showProgress(KiloBundle.message("settings.agentBehavior.skills.openInEditor.pending")) + cs.launch { + val opened = service().openFile(skill.location) + if (opened) return@launch + withContext(edt) { KiloNotifications.error(KiloBundle.message("settings.agentBehavior.skills.openInEditor.failed")) } } } @@ -162,17 +277,16 @@ internal class SkillsSettingsUi( Messages.getQuestionIcon(), ) if (result != Messages.YES) return - mutateAndReload(selectionIndex()) { - if (!service().removeSkill(dir, skill.location)) { - throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.delete.failed")) - } - true - } + state.update { copy(deleted = deleted + skill.location, edited = edited - skill.location) } + view.update(rows(), selectionIndex()) } - private fun config() = service().state.value.config?.skills ?: SkillsConfigDto() + private fun content(skill: SkillDto) = draft.edited[skill.location] ?: skill.content + + private fun config() = app.state.value.config?.skills ?: SkillsConfigDto() private companion object { + const val EDIT_CELL = "edit" const val OPEN_CELL = "open" const val DELETE_CELL = "delete" const val BUILTIN = "builtin" @@ -184,6 +298,77 @@ internal class SkillsSettingsUi( } } +internal interface SkillEditDialogHandle { + fun showAndGet(): Boolean + fun content(): String +} + +private data class SkillsDraft( + val sources: SkillsConfigDto, + val edited: Map = emptyMap(), + val deleted: Set = emptySet(), +) + +private fun skillsDraft(sources: SkillsConfigDto) = SkillsDraft(sources) + +private fun saved(base: SkillsDraft, draft: SkillsDraft): Boolean = base == draft + +internal class SkillEditDialog(private val skill: SkillDto, private val savable: Boolean) : DialogWrapper(true), SkillEditDialogHandle { + private val base = initial() + private val editor = SkillEditor(base, skill.location, savable) + + init { + title = skill.name + setOKButtonText(CommonBundle.getOkButtonText()) + setCancelButtonText(CommonBundle.getCloseButtonText()) + init() + isOKActionEnabled = false + editor.document.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + isOKActionEnabled = savable && editor.text != base + } + }) + } + + override fun createCenterPanel(): JComponent = JBScrollPane(editor).apply { + viewportBorder = editorPad() + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + preferredSize = JBUI.size(720, 520) + } + + override fun createActions() = if (savable) arrayOf(okAction, cancelAction) else arrayOf(cancelAction) + + override fun content() = editor.text + + private fun initial() = skill.content?.takeIf { it.isNotBlank() } + ?: skill.description?.takeIf { it.isNotBlank() } + ?: KiloBundle.message("settings.agentBehavior.skills.content.empty") + + private class SkillEditor(value: String, location: String, editable: Boolean) : EditorTextField( + EditorFactory.getInstance().createDocument(value), + ProjectManager.getInstance().defaultProject, + skillFileType(location), + false, + !editable, + ) { + init { + border = JBUI.Borders.empty() + setOneLineMode(false) + addSettingsProvider { ed -> + ed.setBorder(JBUI.Borders.empty()) + ed.scrollPane.border = JBUI.Borders.empty() + ed.scrollPane.viewportBorder = JBUI.Borders.empty() + ed.settings.isUseSoftWraps = true + ed.settings.isPaintSoftWraps = false + ed.settings.isAdditionalPageAtBottom = false + ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + } + } + } +} + internal class SkillSourcesView( private val parent: SkillsSettingsUi, private val choose: (JComponent) -> String?, @@ -191,19 +376,17 @@ internal class SkillSourcesView( ) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) { private val view = SettingsListView( KiloBundle.message("settings.agentBehavior.skills.sources.empty"), - SettingsListConfig.Preferred.copy(description = false), + SettingsListConfig.Preferred.copy(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION), ) { key, id -> - if (id == DELETE_CELL) remove(key) + if (id == EDIT_CELL) edit(key) } private var cfg = SkillsConfigDto() internal fun sourceList() = view.list init { - border = JBUI.Borders.compound( - JBUI.Borders.customLineTop(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()), - JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0), - ) + border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0) + next(TitledSeparator(KiloBundle.message("settings.agentBehavior.skills.sources.title"))) next(toolbar()) next(JBScrollPane(view).apply { border = null @@ -219,7 +402,12 @@ internal class SkillSourcesView( } private fun toolbar(): JComponent { - val group = DefaultActionGroup(AddPathAction(), AddUrlAction()) + val add = DefaultActionGroup(KiloBundle.message("settings.agentBehavior.skills.sources.add"), true).apply { + templatePresentation.icon = AllIcons.General.Add + add(AddPathAction()) + add(AddUrlAction()) + } + val group = DefaultActionGroup(add, RemoveAction()) val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true) toolbar.targetComponent = this toolbar.updateActionsImmediately() @@ -242,34 +430,42 @@ internal class SkillSourcesView( } private fun rows(config: SkillsConfigDto): List { - val paths = config.paths.map { source(PATH_PREFIX, it, KiloBundle.message("settings.agentBehavior.skills.sources.paths")) } - val urls = config.urls.map { source(URL_PREFIX, it, KiloBundle.message("settings.agentBehavior.skills.sources.urls")) } + val paths = config.paths.map { source(PATH_PREFIX, it) } + val urls = config.urls.map { source(URL_PREFIX, it) } return paths + urls } - private fun source(prefix: String, value: String, section: String) = object : SettingsListItem { + private fun source(prefix: String, value: String) = object : SettingsListItem { override val key = prefix + value override val title = value - override val section = section - override val cells = listOf(SettingsListCell( - DELETE_CELL, - KiloBundle.message("common.delete"), - icon = AllIcons.Actions.GC, - iconOnly = true, - )) + override val doubleClick = EDIT_CELL } - private fun remove(key: String) { - when { - key.startsWith(PATH_PREFIX) -> parent.updateSources(cfg.paths - key.removePrefix(PATH_PREFIX), cfg.urls) - key.startsWith(URL_PREFIX) -> parent.updateSources(cfg.paths, cfg.urls - key.removePrefix(URL_PREFIX)) + internal fun removeSelected() { + val keys = view.selectedItems().map { it.key }.toSet() + if (keys.isEmpty()) return + val paths = cfg.paths.filterNot { PATH_PREFIX + it in keys } + val urls = cfg.urls.filterNot { URL_PREFIX + it in keys } + parent.updateSources(paths, urls) + } + + private fun edit(key: String) { + val path = key.startsWith(PATH_PREFIX) + val old = key.removePrefix(if (path) PATH_PREFIX else URL_PREFIX) + val dialog = SkillSourceDialog(old, path, choose) + if (!dialog.showAndGet()) return + val next = dialog.value().trim().takeIf { it.isNotBlank() } ?: return + if (path) { + parent.updateSources(cfg.paths.map { if (it == old) next else it }.distinct(), cfg.urls) + return } + parent.updateSources(cfg.paths, cfg.urls.map { if (it == old) next else it }.distinct()) } private inner class AddPathAction : DumbAwareAction( KiloBundle.message("settings.agentBehavior.skills.sources.addPath"), null, - AllIcons.General.Add, + null, ) { override fun getActionUpdateThread() = ActionUpdateThread.EDT override fun actionPerformed(e: AnActionEvent) = addPath() @@ -278,25 +474,74 @@ internal class SkillSourcesView( private inner class AddUrlAction : DumbAwareAction( KiloBundle.message("settings.agentBehavior.skills.sources.addUrl"), null, - AllIcons.General.Add, + null, ) { override fun getActionUpdateThread() = ActionUpdateThread.EDT override fun actionPerformed(e: AnActionEvent) = addUrl() } + private inner class RemoveAction : DumbAwareAction( + KiloBundle.message("common.delete"), + null, + AllIcons.General.Remove, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = view.selectedItems().isNotEmpty() + } + override fun actionPerformed(e: AnActionEvent) = removeSelected() + } + private companion object { - const val DELETE_CELL = "delete" + const val EDIT_CELL = "edit" const val PATH_PREFIX = "path:" const val URL_PREFIX = "url:" } } -private fun chooseSkillPath(parent: JComponent): String? { - val descriptor = FileChooserDescriptor(false, true, false, false, false, false).apply { - title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title") - description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt") +private class SkillSourceDialog( + value: String, + private val path: Boolean, + private val choose: (JComponent) -> String?, +) : DialogWrapper(true) { + private val field = JBTextField(value) + + init { + title = if (path) KiloBundle.message("settings.agentBehavior.skills.sources.editPath.title") + else KiloBundle.message("settings.agentBehavior.skills.sources.editUrl.title") + setOKButtonText(KiloBundle.message("common.save")) + init() } - return FileChooser.chooseFile(descriptor, parent, null, null as VirtualFile?)?.path + + override fun createCenterPanel(): JComponent { + if (!path) return field.apply { columns = SOURCE_COLUMNS } + return JPanel(BorderLayout(UiStyle.Gap.sm(), 0)).apply { + add(field.apply { columns = SOURCE_COLUMNS }, BorderLayout.CENTER) + add(JButton("...").apply { + addActionListener { + choose(this)?.let { field.text = it } + } + }, BorderLayout.EAST) + } + } + + fun value() = field.text +} + +private fun chooseSkillPath(parent: JComponent): String? { + return FileChooser.chooseFile(skillPathDescriptor(), parent, null, null as VirtualFile?)?.path +} + +internal fun skillPathDescriptor() = FileChooserDescriptor(false, true, false, false, false, false).apply { + title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title") + description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt") +} + +internal fun skillFileType(location: String): FileType { + val name = location.substringAfterLast('/').substringAfterLast('\\').ifBlank { SKILL_FILE } + val type = FileTypeManager.getInstance().getFileTypeByFileName(name) + if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE + return type } private fun inputSkillUrl(title: String, prompt: String): String? = Messages.showInputDialog( @@ -304,3 +549,14 @@ private fun inputSkillUrl(title: String, prompt: String): String? = Messages.sho title, Messages.getQuestionIcon(), ) + +private fun editorPad() = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), +) + +private const val SOURCE_COLUMNS = 60 +private const val SKILL_FILE = "SKILL.md" +private const val SKILL_LOAD_TIMEOUT_MS = 10_000L diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt index eac5325e2d..0246af99dd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt @@ -8,6 +8,7 @@ import java.awt.Point import java.awt.Rectangle import javax.swing.Icon import javax.swing.JList +import javax.swing.ListSelectionModel import javax.swing.ListCellRenderer import javax.swing.SwingUtilities @@ -22,6 +23,7 @@ internal data class SettingsListConfig( val description: Boolean = true, val descriptionIndent: Boolean = true, val tooltip: Boolean = true, + val selection: Int = ListSelectionModel.SINGLE_SELECTION, ) { companion object { val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL) @@ -44,6 +46,7 @@ internal interface SettingsListItem { val title: String val note: String? get() = null val description: String? get() = null + val doubleClick: String? get() = null val icon: Icon? get() = null val section: String? get() = null val badges: List get() = emptyList() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt index 73eebcda40..18f5f3e3d4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.settings.base import ai.kilocode.client.session.ui.model.ModelSearch +import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.CollectionListModel import com.intellij.ui.ScrollingUtil @@ -8,19 +9,23 @@ import com.intellij.ui.components.JBList import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.xml.util.XmlStringUtil import com.intellij.util.ui.UIUtil +import java.awt.Dimension +import java.awt.Rectangle import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.KeyStroke import javax.swing.ListSelectionModel +import javax.swing.Scrollable +import javax.swing.SwingConstants import javax.swing.event.ListSelectionEvent internal class SettingsListView( empty: String, private val cfg: SettingsListConfig = SettingsListConfig.Equal, private val onCell: (String, String) -> Unit, -) : BaseContentPanel() { +) : BaseContentPanel(), Scrollable { private val model = CollectionListModel() internal val list = object : JBList(model) { override fun getToolTipText(event: MouseEvent): String? { @@ -34,7 +39,7 @@ internal class SettingsListView( return XmlStringUtil.wrapInHtml(text) } }.apply { - selectionMode = ListSelectionModel.SINGLE_SELECTION + selectionMode = cfg.selection setExpandableItemsEnabled(false) emptyText.text = empty } @@ -67,6 +72,11 @@ internal class SettingsListView( val hit = hit(e, enabled = false) ?: return if (hit.id != null) return val item = hit.item + item.doubleClick?.let { id -> + onCell(item.key, id) + e.consume() + return + } primary(item) e.consume() } @@ -94,6 +104,12 @@ internal class SettingsListView( return list.selectedValue } + @RequiresEdt + fun selectedItems(): List { + checkEdt() + return list.selectedValuesList + } + @RequiresEdt fun selectedIndex(): Int { checkEdt() @@ -121,6 +137,7 @@ internal class SettingsListView( @RequiresEdt fun setBusy(value: Boolean) { checkEdt() + list.setPaintBusy(value) if (list.isEnabled == !value) return list.isEnabled = !value list.repaint() @@ -191,9 +208,15 @@ internal class SettingsListView( private fun primary(item: SettingsListItem) { val cells = settingsListVisibleCells(item, true) val cell = cells.firstOrNull { it.enabled && it.primary } - ?: cells.firstOrNull { it.enabled } - ?: return - onCell(item.key, cell.id) + if (cell != null) { + onCell(item.key, cell.id) + return + } + item.doubleClick?.let { id -> + onCell(item.key, id) + return + } + cells.firstOrNull { it.enabled }?.let { onCell(item.key, it.id) } } private fun hit(e: MouseEvent, enabled: Boolean = true): Hit? { @@ -217,6 +240,27 @@ internal class SettingsListView( check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" } } + override fun getScrollableTracksViewportWidth() = true + + override fun getScrollableTracksViewportHeight() = false + + override fun getPreferredScrollableViewportSize(): Dimension = preferredSize + + override fun getScrollableUnitIncrement( + visibleRect: Rectangle, + orientation: Int, + direction: Int, + ): Int { + if (orientation != SwingConstants.VERTICAL) return UiStyle.Gap.pad() + return list.fixedCellHeight.takeIf { it > 0 } ?: UiStyle.Gap.xl() + } + + override fun getScrollableBlockIncrement( + visibleRect: Rectangle, + orientation: Int, + direction: Int, + ) = if (orientation == SwingConstants.VERTICAL) visibleRect.height else visibleRect.width + private data class Hit(val item: SettingsListItem, val id: String?) private data class Press(val key: String, val id: String) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt index cf87dd58ff..240c2c0212 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt @@ -33,6 +33,14 @@ internal open class SettingsPanel : SettingsOverlayPanel() { repaint() } + protected fun setCenter(component: JComponent) { + val layout = content.layout as? BorderLayout + layout?.getLayoutComponent(BorderLayout.CENTER)?.let { content.remove(it) } + content.add(component, BorderLayout.CENTER) + revalidate() + repaint() + } + } private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable { 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 6d0b5ab880..0f658c0c8d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -1,4 +1,5 @@ common.delete=Delete +common.save=Save session.action.cancel=Cancel session.connection.connecting=Loading... @@ -439,19 +440,28 @@ settings.agentBehavior.mcp.status.disabled=disabled settings.agentBehavior.skills.displayName=Skills settings.agentBehavior.skills.search=Filter skills settings.agentBehavior.skills.empty=No skills found. -settings.agentBehavior.skills.open=Open in editor +settings.agentBehavior.skills.content.empty=No skill content available. +settings.agentBehavior.skills.load.timeout=Skill loading timed out. Existing skills were kept; remove slow or unreachable URLs and refresh. +settings.agentBehavior.skills.reload.deferred=Skills source saved. Reload the core after active sessions finish to apply new skills. +settings.agentBehavior.skills.reload.blocked=Skills source saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills. +settings.agentBehavior.skills.saved.notification=Skills settings saved settings.agentBehavior.skills.delete.title=Delete Skill settings.agentBehavior.skills.delete.message=Delete skill {0}? This removes the skill file and cannot be undone. settings.agentBehavior.skills.delete.failed=Could not delete the skill. +settings.agentBehavior.skills.openInEditor=Open in Editor +settings.agentBehavior.skills.openInEditor.pending=The skill file will open after you close Settings. +settings.agentBehavior.skills.openInEditor.failed=Could not open the skill file in the editor. settings.agentBehavior.skills.sources.empty=No skill sources configured. -settings.agentBehavior.skills.sources.paths=Paths -settings.agentBehavior.skills.sources.urls=URLs +settings.agentBehavior.skills.sources.title=Additional Skill Sources +settings.agentBehavior.skills.sources.add=Add settings.agentBehavior.skills.sources.addPath=Add path settings.agentBehavior.skills.sources.addPath.title=Add Skill Path settings.agentBehavior.skills.sources.addPath.prompt=Choose a folder containing Kilo skills. settings.agentBehavior.skills.sources.addUrl=Add URL settings.agentBehavior.skills.sources.addUrl.title=Add Skill URL settings.agentBehavior.skills.sources.addUrl.prompt=Enter a skill source URL. +settings.agentBehavior.skills.sources.editPath.title=Edit Skill Path +settings.agentBehavior.skills.sources.editUrl.title=Edit Skill URL settings.providers.loading=Loading providers... settings.providers.connected=Connected providers settings.providers.available=Available providers diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt index e671b69ee2..2296ccc658 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.settings.agents import ai.kilocode.client.app.KiloAgentBehaviorService import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloWorkspaceService -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.settings.base.SettingsListItem import ai.kilocode.client.settings.base.settingsListCellBounds import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi @@ -16,26 +15,35 @@ import ai.kilocode.rpc.dto.KiloAppStatusDto import ai.kilocode.rpc.dto.SkillDto import ai.kilocode.rpc.dto.SkillsConfigDto import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType +import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.TestDialog import com.intellij.openapi.ui.TestDialogManager import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.replaceService +import com.intellij.ui.TitledSeparator import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane import com.intellij.util.ui.UIUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import java.awt.BorderLayout import java.awt.Container import java.awt.Dimension import java.awt.Point import java.awt.event.InputEvent import java.awt.event.MouseEvent import javax.swing.JComponent +import javax.swing.ScrollPaneConstants +import javax.swing.Scrollable import javax.swing.JTextField class SkillsSettingsUiTest : BasePlatformTestCase() { @@ -69,23 +77,49 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals("plan", custom.title) assertEquals(CUSTOM, custom.note) assertEquals("Plan work", custom.description) - assertEquals(listOf("open", "delete"), custom.cells.map { it.id }) - val open = custom.cells.single { it.id == "open" } - assertEquals(KiloBundle.message("settings.agentBehavior.skills.open"), open.label) - assertTrue(open.primary) - assertFalse(open.iconOnly) - assertNull(open.icon) + assertEquals("edit", custom.doubleClick) + assertEquals(listOf("open", "edit", "delete"), custom.cells.map { it.id }) + assertTrue(custom.cells.single { it.id == "open" }.primary) + assertFalse(custom.cells.single { it.id == "edit" }.primary) assertTrue(custom.cells.single { it.id == "delete" }.iconOnly) val builtin = rows.single { it.key == "builtin" } assertEquals("thinking", builtin.title) assertNull(builtin.note) + assertEquals("edit", builtin.doubleClick) assertEquals(listOf("built-in"), builtin.badges.map { it.text }) - assertTrue(builtin.cells.isEmpty()) + assertEquals(listOf("edit"), builtin.cells.map { it.id }) assertEquals(listOf(DIR), agentRpc.skillCalls) true } } + fun `test skills list is vertically scrolled without horizontal scrollbar`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + + edt { + val pane = scrollFor(panel, skillsList(panel)) + val view = pane.viewport.view + val layout = panel.content.layout as BorderLayout + + assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, pane.horizontalScrollBarPolicy) + assertTrue((view as Scrollable).getScrollableTracksViewportWidth()) + assertFalse(view.getScrollableTracksViewportHeight()) + assertSame(pane, layout.getLayoutComponent(BorderLayout.CENTER)) + assertSame(panel.sources, layout.getLayoutComponent(BorderLayout.SOUTH)) + true + } + } + + fun `test sources section has additional sources title`() { + val panel = panel() + flushUntil { sourceRows(panel).size == 2 } + + assertTrue(edt { + components(panel).filterIsInstance().any { it.text == "Additional Skill Sources" } + }) + } + fun `test skills list does not show description tooltips`() { val panel = panel() flushUntil { rows(panel).size == 2 } @@ -121,25 +155,83 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { } } - fun `test open action calls direct open file`() { + fun `test double click stages skill content until apply`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") }) + flushUntil { rows(panel).size == 2 } + + doubleClick(skillsList(panel), panel, CUSTOM) + + assertTrue(edt { panel.modified() }) + assertTrue(agentRpc.skillSaves.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { agentRpc.skillSaves.size == 1 } + assertEquals(Triple(DIR, CUSTOM, "# Saved"), agentRpc.skillSaves.single()) + } + + fun `test edited skill row keeps normal actions`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Draft") }) + flushUntil { rows(panel).size == 2 } + + doubleClick(skillsList(panel), panel, CUSTOM) + + assertEquals(listOf("open", "edit", "delete"), edt { rows(panel).single { it.key == CUSTOM }.cells.map { it.id } }) + assertTrue(edt { panel.modified() }) + } + + fun `test reopening staged skill edit shows draft content before apply`() { + val seen = mutableListOf() + val panel = panel(edit = { skill, _ -> + seen += skill.content + FakeSkillDialog(if (seen.size == 1) "# Draft" else "# Draft 2") + }) + flushUntil { rows(panel).size == 2 } + + doubleClick(skillsList(panel), panel, CUSTOM) + doubleClick(skillsList(panel), panel, CUSTOM) + + assertEquals(listOf("# Plan\nUse steps", "# Draft"), seen) + assertTrue(agentRpc.skillSaves.isEmpty()) + } + + fun `test open in editor action opens skill file`() { val panel = panel() flushUntil { rows(panel).size == 2 } click(skillsList(panel), panel, CUSTOM, "open") - flushUntil { workspaceRpc.opened.contains(CUSTOM) } - assertEquals(listOf(CUSTOM), workspaceRpc.opened) - assertTrue(workspaceRpc.fileCalls.isEmpty()) + assertEquals("The skill file will open after you close Settings.", edt { progressText(panel) }) + flushUntil { workspaceRpc.openedFiles.size == 1 } + assertEquals(FakeWorkspaceRpcApi.Opened(CUSTOM, null, null), workspaceRpc.openedFiles.single()) } - fun `test delete action removes skill and reloads`() { + fun `test skill edit dialog shows content with fallback`() { + edt { + val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true) + val fallback = SkillEditDialog(SkillDto("plan", "desc", CUSTOM), true) + try { + assertEquals("# Plan\nUse steps", content.content()) + assertEquals("desc", fallback.content()) + assertEquals("OK", content.okText()) + } finally { + content.close(DialogWrapper.CANCEL_EXIT_CODE) + fallback.close(DialogWrapper.CANCEL_EXIT_CODE) + } + true + } + } + + + fun `test delete action stages skill removal until apply`() { val panel = panel() flushUntil { rows(panel).size == 2 } TestDialogManager.setTestDialog(TestDialog.YES) click(skillsList(panel), panel, CUSTOM, "delete") - flushUntil { rows(panel).none { it.key == CUSTOM } } + assertTrue(edt { rows(panel).none { it.key == CUSTOM } }) + assertTrue(agentRpc.skillRemovals.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { agentRpc.skillRemovals.size == 1 } assertEquals(listOf(DIR to CUSTOM), agentRpc.skillRemovals) } @@ -155,34 +247,107 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertTrue(edt { rows(panel).any { it.key == CUSTOM } }) } - fun `test add path and url write skills config patch`() { + fun `test add path and url write skills config patch on apply`() { var path = "/extra/skills" var url = "https://skills.test/index.json" val panel = panel(choose = { path }, input = { _, _ -> url }) flushUntil { rows(panel).size == 2 } edt { panel.sources.addPath(); true } - flushUntil { appRpc.configPatches.size == 1 } - flushUntil { edt { skillsList(panel).isEnabled } } edt { panel.sources.addUrl(); true } - flushUntil { appRpc.configPatches.size == 2 } + flushUntil { sourceRows(panel).any { it.key == "url:$url" } } + assertTrue(appRpc.configPatches.isEmpty()) - val paths = appRpc.configPatches.first().skills!!.paths - val urls = appRpc.configPatches.last().skills!!.urls + edt { panel.applyDraft(); true } + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + + val paths = appRpc.configPatches.single().skills!!.paths + val urls = appRpc.configPatches.single().skills!!.urls assertEquals(listOf("/global/skills", path), paths) assertEquals(listOf("https://skills.test/base.json", url), urls) + assertEquals( + listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"), + edt { sourceRows(panel).map { it.key } }, + ) + assertTrue(agentRpc.skillReloads.isEmpty()) + } + + fun `test stale config update result keeps added skill sources visible`() { + val path = "/extra/skills" + val url = "https://skills.test/index.json" + val extra = "$path/extra/SKILL.md" + val panel = panel(choose = { path }, input = { _, _ -> url }) + appRpc.configUpdateReturnStale = true + appRpc.afterConfig = { agentRpc.skills = agentRpc.skills + SkillDto("extra", "Extra skill", extra) } + flushUntil { rows(panel).size == 2 } + + edt { + panel.sources.addPath() + panel.sources.addUrl() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertTrue(edt { rows(panel).any { it.key == extra } }) + assertEquals( + listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"), + edt { sourceRows(panel).map { it.key } }, + ) + } + + fun `test source reset discards staged changes`() { + val path = "/extra/skills" + val panel = panel(choose = { path }) + flushUntil { rows(panel).size == 2 } + + edt { panel.sources.addPath(); true } + + assertTrue(edt { sourceRows(panel).any { it.key == "path:$path" } }) + assertTrue(edt { panel.modified() }) + edt { panel.resetDraft(); true } + + assertTrue(appRpc.configPatches.isEmpty()) + assertEquals(listOf(CUSTOM, "builtin"), edt { rows(panel).map { it.key } }) + assertFalse(edt { sourceRows(panel).any { it.key == "path:$path" } }) + assertTrue(agentRpc.skillReloads.isEmpty()) } fun `test delete source writes skills config patch`() { val panel = panel() flushUntil { rows(panel).size == 2 && sourceRows(panel).size == 2 } - click(sourceList(panel), panel, "path:/global/skills", "delete") + edt { + sourceList(panel).selectedIndices = intArrayOf(0) + panel.sources.removeSelected() + true + } - flushUntil { appRpc.configPatches.size == 1 } + assertTrue(appRpc.configPatches.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } val patch = appRpc.configPatches.single().skills!! assertEquals(emptyList(), patch.paths) assertEquals(listOf("https://skills.test/base.json"), patch.urls) + assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) + } + + fun `test stale config update result keeps removed skill sources hidden`() { + val panel = panel() + appRpc.configUpdateReturnStale = true + appRpc.afterConfig = { agentRpc.skills = agentRpc.skills.filterNot { it.location == CUSTOM } } + flushUntil { rows(panel).size == 2 && sourceRows(panel).size == 2 } + + edt { + sourceList(panel).selectedIndices = intArrayOf(0) + panel.sources.removeSelected() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertEquals(listOf("builtin"), edt { rows(panel).map { it.key } }) + assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) } fun `test search filters skills by name`() { @@ -198,12 +363,40 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { flushUntil { rows(panel).map { it.key } == listOf("builtin") } } + fun `test skills reload failure keeps existing rows`() { + val panel = panel() + flushUntil { rows(panel).size == 2 } + agentRpc.skillsError = RuntimeException("timeout") + + edt { panel.reload(); true } + flushUntil { edt { skillsList(panel).isEnabled } } + + assertEquals(listOf(CUSTOM, "builtin"), edt { rows(panel).map { it.key } }) + } + + fun `test skill editor file type follows location extension`() { + assertNotSame(UnknownFileType.INSTANCE, skillFileType("/tmp/skills/plan/SKILL.md")) + assertEquals( + FileTypeManager.getInstance().getFileTypeByFileName("index.html"), + skillFileType("/tmp/skills/index.html"), + ) + assertEquals(PlainTextFileType.INSTANCE, skillFileType("/tmp/skills/index.unknown")) + } + + fun `test skill path chooser accepts directories only`() { + val descriptor = skillPathDescriptor() + + assertTrue(descriptor.isChooseFolders) + assertFalse(descriptor.isChooseFiles) + } + private fun panel( choose: (JComponent) -> String? = { null }, input: (String, String) -> String? = { _, _ -> null }, + edit: (SkillDto, Boolean) -> SkillEditDialogHandle = { _, _ -> FakeSkillDialog("# Plan\nUse steps") }, ): SkillsSettingsUi { install() - val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, input) } + val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, input, edit) } ui = panel edt { panel.reload(); true } return panel @@ -213,13 +406,13 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { val cs = CoroutineScope(SupervisorJob()) scope = cs appRpc = FakeAppRpcApi() + workspaceRpc = FakeWorkspaceRpcApi() agentRpc = FakeAgentBehaviorRpcApi().apply { skills = listOf( - SkillDto("plan", "Plan work", CUSTOM), - SkillDto("thinking", "Built in", "builtin"), + SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps"), + SkillDto("thinking", "Built in", "builtin", "Built in content"), ) } - workspaceRpc = FakeWorkspaceRpcApi() app = KiloAppService(cs, appRpc) val ready = KiloAppStateDto( KiloAppStatusDto.READY, @@ -248,6 +441,18 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { } } + private fun doubleClick(list: JBList, panel: SkillsSettingsUi, key: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val idx = rows(panel).indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = list.getCellBounds(idx, idx) + fire(list, mouse(list, MouseEvent.MOUSE_CLICKED, center(area), count = 2)) + true + } + } + private fun rows(panel: SkillsSettingsUi): List = items(skillsList(panel)) private fun sourceRows(panel: SkillsSettingsUi): List = items(sourceList(panel)) @@ -261,6 +466,18 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { private fun sourceList(panel: SkillsSettingsUi) = components(panel).filterIsInstance>().last() + private fun scrollFor(panel: SkillsSettingsUi, list: JBList) = components(panel) + .filterIsInstance() + .single { pane -> pane.viewport.view === list.parent } + + private fun progressText(panel: SkillsSettingsUi) = components(panel.progress).filterIsInstance().single().text + + private fun SkillEditDialog.okText(): String { + val method = DialogWrapper::class.java.getDeclaredMethod("getOKAction") + method.isAccessible = true + return (method.invoke(this) as javax.swing.Action).getValue(javax.swing.Action.NAME) as String + } + private fun components(root: java.awt.Component): List { val out = mutableListOf() fun visit(item: java.awt.Component) { @@ -284,14 +501,14 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point)) } - private fun mouse(list: JBList, id: Int, point: Point) = MouseEvent( + private fun mouse(list: JBList, id: Int, point: Point, count: Int = 1) = MouseEvent( list, id, System.currentTimeMillis(), if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0, point.x, point.y, - 1, + count, false, MouseEvent.BUTTON1, ) @@ -318,3 +535,8 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md" } } + +private class FakeSkillDialog(private val text: String) : SkillEditDialogHandle { + override fun showAndGet() = true + override fun content() = text +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index ca7f5cd7c8..46c9c1399f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -14,6 +14,8 @@ import java.awt.Dimension import java.awt.Point import java.awt.event.InputEvent import java.awt.event.MouseEvent +import javax.swing.Scrollable +import javax.swing.SwingConstants import javax.swing.SwingUtilities class SettingsListViewTest : BasePlatformTestCase() { @@ -260,6 +262,17 @@ class SettingsListViewTest : BasePlatformTestCase() { } } + fun `test list view tracks viewport width`() { + edt { + val view = SettingsListView("Empty") { _, _ -> } + view.update(listOf(item("long", "Alpha", "A very long description that should wrap instead of scrolling"))) + + assertTrue((view as Scrollable).getScrollableTracksViewportWidth()) + assertFalse(view.getScrollableTracksViewportHeight()) + assertEquals(160, view.getScrollableBlockIncrement(java.awt.Rectangle(0, 0, 320, 160), SwingConstants.VERTICAL, 1)) + } + } + private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem { override val key = id override val title = name diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt index bb11472b06..8b69e89a68 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt @@ -17,6 +17,8 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { val agentCalls = mutableListOf() val skillCalls = mutableListOf() val skillRemovals = mutableListOf>() + val skillReloads = mutableListOf() + val skillSaves = mutableListOf>() val mcpCalls = mutableListOf() val mcpConfigCalls = mutableListOf() val mcpSaves = mutableListOf>() @@ -30,12 +32,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var afterRemove: (suspend (String, String) -> Unit)? = null var afterMcpConnect: (suspend (String, String) -> Unit)? = null var createError: Exception? = null + var skillsError: Exception? = null var removeError: Exception? = null var removeSkillError: Exception? = null + var saveSkillError: Exception? = null var mcpStatusError: Exception? = null var mcpConnectError: Exception? = null var removeResult = true var removeSkillResult = true + var reloadSkillResult = true + var saveSkillResult = true var mcpConnectResult = true var mcpDisconnectResult = true var mcpAuthenticateResult = true @@ -48,6 +54,7 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { override suspend fun skills(directory: String): List { assertNotEdt("agentBehavior.skills") + skillsError?.let { throw it } skillCalls.add(directory) return skills } @@ -60,6 +67,20 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { return removeSkillResult } + override suspend fun reloadSkills(directory: String): Boolean { + assertNotEdt("agentBehavior.reloadSkills") + skillReloads.add(directory) + return reloadSkillResult + } + + override suspend fun saveSkill(directory: String, location: String, content: String): Boolean { + assertNotEdt("agentBehavior.saveSkill") + saveSkillError?.let { throw it } + skillSaves.add(Triple(directory, location, content)) + if (saveSkillResult) skills = skills.map { if (it.location == location) it.copy(content = content) else it } + return saveSkillResult + } + override suspend fun removeAgent(directory: String, name: String): Boolean { assertNotEdt("agentBehavior.removeAgent") removeError?.let { throw it } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt index 345ab0657c..eaa2223045 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt @@ -26,6 +26,10 @@ interface KiloAgentBehaviorRpcApi : RemoteApi { suspend fun removeSkill(directory: String, location: String): Boolean + suspend fun reloadSkills(directory: String): Boolean + + suspend fun saveSkill(directory: String, location: String, content: String): Boolean + suspend fun removeAgent(directory: String, name: String): Boolean suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt index 6bdd4f9805..386f43bc18 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt @@ -7,4 +7,5 @@ data class SkillDto( val name: String, val description: String? = null, val location: String, + val content: String? = null, ) From aae154c7601da0d6f21fd9f7758c16cce7db42a4 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 12:07:28 -0400 Subject: [PATCH 3/9] fix(jetbrains): detect skill editor syntax --- .../settings/agents/SkillsConfigurable.kt | 24 +++++++++++++++---- .../resources/messages/KiloBundle.properties | 1 + .../settings/agents/SkillsSettingsUiTest.kt | 17 +++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index 2caae3017b..807a0d0c59 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -238,7 +238,7 @@ internal class SkillsSettingsUi( ).takeUnless { builtin(skill.location) }, SettingsListCell( EDIT_CELL, - KiloBundle.message("settings.agentBehavior.edit"), + KiloBundle.message(if (builtin(skill.location)) "common.open" else "settings.agentBehavior.edit"), primary = builtin(skill.location), ), SettingsListCell( @@ -348,7 +348,7 @@ internal class SkillEditDialog(private val skill: SkillDto, private val savable: private class SkillEditor(value: String, location: String, editable: Boolean) : EditorTextField( EditorFactory.getInstance().createDocument(value), ProjectManager.getInstance().defaultProject, - skillFileType(location), + skillFileType(location, value), false, !editable, ) { @@ -537,13 +537,29 @@ internal fun skillPathDescriptor() = FileChooserDescriptor(false, true, false, f description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt") } -internal fun skillFileType(location: String): FileType { - val name = location.substringAfterLast('/').substringAfterLast('\\').ifBlank { SKILL_FILE } +internal fun skillFileType(location: String, content: String? = null): FileType { + val syntax = content?.syntaxName() + val name = syntax ?: location.substringAfterLast('/').substringAfterLast('\\').ifBlank { SKILL_FILE } val type = FileTypeManager.getInstance().getFileTypeByFileName(name) if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE return type } +private fun String.syntaxName(): String? { + val text = trimStart() + if (text.isBlank()) return null + if (text.looksHtml()) return "index.html" + if (text.looksMarkdown()) return SKILL_FILE + return null +} + +private fun String.looksHtml() = contains(Regex("^\\s*( + line.matches(Regex("\\s{0,3}(#{1,6}\\s+.+|[-*+]\\s+.+|\\d+\\.\\s+.+|```.*|>\\s+.+)")) || + line.contains(Regex("(`[^`]+`|\\[[^]]+][(][^)]+[)])")) +} + private fun inputSkillUrl(title: String, prompt: String): String? = Messages.showInputDialog( prompt, title, 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 0f658c0c8d..066a8da227 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -1,4 +1,5 @@ common.delete=Delete +common.open=Open common.save=Save session.action.cancel=Cancel diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt index 2296ccc658..8e5736323e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -81,6 +81,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals(listOf("open", "edit", "delete"), custom.cells.map { it.id }) assertTrue(custom.cells.single { it.id == "open" }.primary) assertFalse(custom.cells.single { it.id == "edit" }.primary) + assertEquals("Edit", custom.cells.single { it.id == "edit" }.label) assertTrue(custom.cells.single { it.id == "delete" }.iconOnly) val builtin = rows.single { it.key == "builtin" } assertEquals("thinking", builtin.title) @@ -88,6 +89,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals("edit", builtin.doubleClick) assertEquals(listOf("built-in"), builtin.badges.map { it.text }) assertEquals(listOf("edit"), builtin.cells.map { it.id }) + assertEquals("Open", builtin.cells.single().label) assertEquals(listOf(DIR), agentRpc.skillCalls) true } @@ -208,18 +210,33 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { edt { val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true) val fallback = SkillEditDialog(SkillDto("plan", "desc", CUSTOM), true) + val readonly = SkillEditDialog(SkillDto("kilo-config", "desc", "builtin", "

Kilo Config

"), false) try { assertEquals("# Plan\nUse steps", content.content()) assertEquals("desc", fallback.content()) + assertEquals("

Kilo Config

", readonly.content()) assertEquals("OK", content.okText()) } finally { content.close(DialogWrapper.CANCEL_EXIT_CODE) fallback.close(DialogWrapper.CANCEL_EXIT_CODE) + readonly.close(DialogWrapper.CANCEL_EXIT_CODE) } true } } + fun `test skill editor file type follows content syntax before location`() { + assertEquals( + FileTypeManager.getInstance().getFileTypeByFileName("index.html"), + skillFileType("builtin", "

Kilo CLI Configuration Reference

All config lives in kilo.json.

"), + ) + assertEquals( + skillFileType("SKILL.md"), + skillFileType("builtin", "# Kilo CLI Configuration Reference\n\nAll config lives in `kilo.json`."), + ) + assertEquals(PlainTextFileType.INSTANCE, skillFileType("builtin", "Plain fallback text")) + } + fun `test delete action stages skill removal until apply`() { val panel = panel() From edb9098a70f2272cbd4a54a4db02369ae6041aa2 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 12:53:52 -0400 Subject: [PATCH 4/9] fix(jetbrains): treat remote skills as read-only --- .../kilocode/backend/cli/KiloCliDataParser.kt | 8 ++- .../rpc/KiloAgentBehaviorRpcApiImpl.kt | 37 +++++++++- .../backend/rpc/KiloWorkspaceDtoMapper.kt | 1 + .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 18 +++++ .../settings/agents/SkillsConfigurable.kt | 16 +++-- .../settings/agents/SkillsSettingsUiTest.kt | 71 +++++++++++++------ .../kotlin/ai/kilocode/rpc/dto/SkillDto.kt | 1 + 7 files changed, 121 insertions(+), 31 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 868a1c6306..73fd3058db 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -625,7 +625,13 @@ object KiloCliDataParser { val obj = item.obj() ?: return@mapNotNull null val name = obj.str("name") ?: return@mapNotNull null val location = obj.str("location") ?: return@mapNotNull null - SkillDto(name = name, description = obj.str("description"), location = location, content = obj.str("content")) + SkillDto( + name = name, + description = obj.str("description"), + location = location, + content = obj.str("content"), + editable = obj.bool("editable"), + ) } fun parseAgentBehaviorCommands(raw: String): List = diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index 9d3e505608..1a85c74c37 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -23,6 +23,7 @@ import kotlinx.serialization.json.JsonPrimitive import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import com.intellij.openapi.util.SystemInfo import java.net.URLEncoder import java.nio.file.Files import java.nio.file.InvalidPathException @@ -63,7 +64,10 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = override suspend fun skills(directory: String): List { val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) - return items.map { item -> item.copy(content = skillContent(item) ?: item.content) } + return items.map { item -> + val editable = editable(item) + item.copy(content = skillContent(item) ?: item.content, editable = editable) + } } override suspend fun removeSkill(directory: String, location: String): Boolean = @@ -225,6 +229,35 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = }.getOrNull() } + private fun editable(skill: SkillDto): Boolean { + val raw = normalizeWorkspacePath(skill.location) ?: return false + val path = try { + Path.of(raw).normalize() + } catch (_: InvalidPathException) { + return false + } + if (!path.isAbsolute || !isSkillFile(path)) return false + if (urlCached(path)) return false + return true + } + + private fun urlCached(path: Path): Boolean { + val root = Path.of(cacheRoot(), "kilo", "skills").normalize() + if (path.startsWith(root)) return true + val parts = (0 until path.nameCount).map { path.getName(it).toString() } + return parts.windowed(3).any { it[1] == "kilo" && it[2] == "skills" && it[0] in cacheNames } + } + + private fun cacheRoot(): String { + val xdg = System.getenv("XDG_CACHE_HOME")?.takeIf { it.isNotBlank() } + if (xdg != null) return xdg + val home = System.getProperty("user.home") + if (SystemInfo.isMac) return Path.of(home, "Library", "Caches").toString() + if (SystemInfo.isWindows) return System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() } + ?: Path.of(home, "AppData", "Local").toString() + return Path.of(home, ".cache").toString() + } + private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) { val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable") val url = "http://127.0.0.1:${app.port}$path" @@ -333,3 +366,5 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = val config: McpConfigDto?, ) } + +private val cacheNames = setOf(".cache", "cache", "Caches") diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index 395ef9b4de..981ea1ad28 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -65,6 +65,7 @@ internal object KiloWorkspaceDtoMapper { description = s.description, location = s.location, content = s.content, + editable = false, ) private fun provider(p: ProviderInfo) = ProviderDto( diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index eaebbb1089..fad12e7cfb 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import java.nio.file.Files +import java.nio.file.Path import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertContains @@ -101,6 +102,8 @@ class KiloAgentBehaviorRpcApiImplTest { assertEquals(listOf("plan", "builtin"), skills.map { it.name }) assertEquals("Plan work", skills.single { it.name == "plan" }.description) assertEquals(content, skills.single { it.name == "plan" }.content) + assertEquals(true, skills.single { it.name == "plan" }.editable) + assertEquals(false, skills.single { it.name == "builtin" }.editable) assertTrue(rpc.removeSkill("/test project", file.toString())) assertEquals("{\"location\":\"$file\"}", mock.lastSkillRemoveBody) @@ -116,6 +119,21 @@ class KiloAgentBehaviorRpcApiImplTest { assertEquals(1, mock.requestCount("/instance/reload")) } + @Test + fun `url cached skills are read only`() = runBlocking { + val cache = Path.of(System.getProperty("user.home"), ".cache", "kilo", "skills", "remote") + val file = Files.createDirectories(cache).resolve("SKILL.md") + Files.writeString(file, "# Remote") + mock.skills = """[ + {"name":"remote","description":"Remote","location":"$file","content":"# Remote"} + ]""".trimIndent() + + val skill = rpc().skills("/test project").single() + + assertEquals(false, skill.editable) + assertEquals("# Remote", skill.content) + } + @Test fun `save skill supports configured markdown text and html files without reload`() = runBlocking { val dir = Files.createTempDirectory("kilo-skill-test") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index 807a0d0c59..164434f6de 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -235,31 +235,35 @@ internal class SkillsSettingsUi( OPEN_CELL, KiloBundle.message("settings.agentBehavior.skills.openInEditor"), primary = true, - ).takeUnless { builtin(skill.location) }, + ).takeIf { skill.editable }, SettingsListCell( EDIT_CELL, - KiloBundle.message(if (builtin(skill.location)) "common.open" else "settings.agentBehavior.edit"), - primary = builtin(skill.location), + KiloBundle.message(if (skill.editable) "settings.agentBehavior.edit" else "common.open"), + primary = !skill.editable, ), SettingsListCell( DELETE_CELL, KiloBundle.message("common.delete"), icon = AllIcons.Actions.GC, iconOnly = true, - ).takeUnless { builtin(skill.location) }, + ).takeIf { skill.editable }, ) } private fun edit(skill: SkillDto) { val current = skill.copy(content = content(skill)) - val dialog = edit(current, !builtin(skill.location)) + val dialog = edit(current, skill.editable) + if (!skill.editable) { + dialog.showAndGet() + return + } if (!dialog.showAndGet()) return state.update { copy(edited = edited + (skill.location to dialog.content())) } view.update(rows(), SettingsListSelection.Key(key(skill))) } private fun open(skill: SkillDto) { - if (builtin(skill.location)) return + if (!skill.editable) return showProgress(KiloBundle.message("settings.agentBehavior.skills.openInEditor.pending")) cs.launch { val opened = service().openFile(skill.location) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt index 8e5736323e..fbde73c460 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -53,6 +53,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { private lateinit var appRpc: FakeAppRpcApi private lateinit var agentRpc: FakeAgentBehaviorRpcApi private lateinit var workspaceRpc: FakeWorkspaceRpcApi + private var shown = 0 override fun tearDown() { try { @@ -69,7 +70,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test loads skills with location note and builtins have no actions`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { val rows = rows(panel) @@ -90,6 +91,9 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals(listOf("built-in"), builtin.badges.map { it.text }) assertEquals(listOf("edit"), builtin.cells.map { it.id }) assertEquals("Open", builtin.cells.single().label) + val remote = rows.single { it.key == REMOTE } + assertEquals(listOf("edit"), remote.cells.map { it.id }) + assertEquals("Open", remote.cells.single().label) assertEquals(listOf(DIR), agentRpc.skillCalls) true } @@ -97,7 +101,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test skills list is vertically scrolled without horizontal scrollbar`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { val pane = scrollFor(panel, skillsList(panel)) @@ -124,7 +128,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test skills list does not show description tooltips`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { val list = skillsList(panel) @@ -139,7 +143,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test renderer puts location on first line and description on preview line`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { val list = skillsList(panel) @@ -159,7 +163,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test double click stages skill content until apply`() { val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") }) - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } doubleClick(skillsList(panel), panel, CUSTOM) @@ -172,7 +176,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test edited skill row keeps normal actions`() { val panel = panel(edit = { _, _ -> FakeSkillDialog("# Draft") }) - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } doubleClick(skillsList(panel), panel, CUSTOM) @@ -186,7 +190,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { seen += skill.content FakeSkillDialog(if (seen.size == 1) "# Draft" else "# Draft 2") }) - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } doubleClick(skillsList(panel), panel, CUSTOM) doubleClick(skillsList(panel), panel, CUSTOM) @@ -197,7 +201,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test open in editor action opens skill file`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } click(skillsList(panel), panel, CUSTOM, "open") @@ -206,6 +210,22 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals(FakeWorkspaceRpcApi.Opened(CUSTOM, null, null), workspaceRpc.openedFiles.single()) } + fun `test read only skills open without staging edits or editor file open`() { + shown = 0 + val panel = panel(edit = { _, savable -> + assertFalse(savable) + FakeSkillDialog("# Ignored") { shown += 1 } + }) + flushUntil { rows(panel).size == 3 } + + click(skillsList(panel), panel, REMOTE, "edit") + + assertEquals(1, shown) + assertFalse(edt { panel.modified() }) + assertTrue(agentRpc.skillSaves.isEmpty()) + assertTrue(workspaceRpc.openedFiles.isEmpty()) + } + fun `test skill edit dialog shows content with fallback`() { edt { val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true) @@ -240,7 +260,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test delete action stages skill removal until apply`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } TestDialogManager.setTestDialog(TestDialog.YES) click(skillsList(panel), panel, CUSTOM, "delete") @@ -254,7 +274,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test delete action requires confirmation`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } TestDialogManager.setTestDialog { Messages.NO } click(skillsList(panel), panel, CUSTOM, "delete") @@ -268,7 +288,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { var path = "/extra/skills" var url = "https://skills.test/index.json" val panel = panel(choose = { path }, input = { _, _ -> url }) - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { panel.sources.addPath(); true } edt { panel.sources.addUrl(); true } @@ -296,7 +316,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { val panel = panel(choose = { path }, input = { _, _ -> url }) appRpc.configUpdateReturnStale = true appRpc.afterConfig = { agentRpc.skills = agentRpc.skills + SkillDto("extra", "Extra skill", extra) } - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { panel.sources.addPath() @@ -316,7 +336,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test source reset discards staged changes`() { val path = "/extra/skills" val panel = panel(choose = { path }) - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { panel.sources.addPath(); true } @@ -325,14 +345,14 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { edt { panel.resetDraft(); true } assertTrue(appRpc.configPatches.isEmpty()) - assertEquals(listOf(CUSTOM, "builtin"), edt { rows(panel).map { it.key } }) + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) assertFalse(edt { sourceRows(panel).any { it.key == "path:$path" } }) assertTrue(agentRpc.skillReloads.isEmpty()) } fun `test delete source writes skills config patch`() { val panel = panel() - flushUntil { rows(panel).size == 2 && sourceRows(panel).size == 2 } + flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 } edt { sourceList(panel).selectedIndices = intArrayOf(0) @@ -353,7 +373,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { val panel = panel() appRpc.configUpdateReturnStale = true appRpc.afterConfig = { agentRpc.skills = agentRpc.skills.filterNot { it.location == CUSTOM } } - flushUntil { rows(panel).size == 2 && sourceRows(panel).size == 2 } + flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 } edt { sourceList(panel).selectedIndices = intArrayOf(0) @@ -363,13 +383,13 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { } flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } - assertEquals(listOf("builtin"), edt { rows(panel).map { it.key } }) + assertEquals(listOf("builtin", REMOTE), edt { rows(panel).map { it.key } }) assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) } fun `test search filters skills by name`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } edt { components(panel).filterIsInstance().single().text = "think" @@ -382,13 +402,13 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { fun `test skills reload failure keeps existing rows`() { val panel = panel() - flushUntil { rows(panel).size == 2 } + flushUntil { rows(panel).size == 3 } agentRpc.skillsError = RuntimeException("timeout") edt { panel.reload(); true } flushUntil { edt { skillsList(panel).isEnabled } } - assertEquals(listOf(CUSTOM, "builtin"), edt { rows(panel).map { it.key } }) + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) } fun `test skill editor file type follows location extension`() { @@ -426,8 +446,9 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { workspaceRpc = FakeWorkspaceRpcApi() agentRpc = FakeAgentBehaviorRpcApi().apply { skills = listOf( - SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps"), + SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps", editable = true), SkillDto("thinking", "Built in", "builtin", "Built in content"), + SkillDto("remote", "Remote skill", REMOTE, "# Remote skill"), ) } app = KiloAppService(cs, appRpc) @@ -550,10 +571,14 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { private companion object { const val DIR = "/test" const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md" + const val REMOTE = "/home/test/.cache/kilo/skills/remote/SKILL.md" } } -private class FakeSkillDialog(private val text: String) : SkillEditDialogHandle { - override fun showAndGet() = true +private class FakeSkillDialog(private val text: String, private val show: () -> Unit = {}) : SkillEditDialogHandle { + override fun showAndGet(): Boolean { + show() + return true + } override fun content() = text } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt index 386f43bc18..50dfee7adf 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt @@ -8,4 +8,5 @@ data class SkillDto( val description: String? = null, val location: String, val content: String? = null, + val editable: Boolean = false, ) From 717bfea327ea8188c929984a7535ff5a982219b4 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 14:03:15 -0400 Subject: [PATCH 5/9] fix(jetbrains): address skills settings review --- .../rpc/KiloAgentBehaviorRpcApiImpl.kt | 20 +++++++++++ .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 22 +++++++++++++ .../client/app/KiloAgentBehaviorService.kt | 6 +++- .../settings/agents/SkillsConfigurable.kt | 13 ++++++-- .../client/settings/base/SettingsListView.kt | 2 +- .../resources/messages/KiloBundle.properties | 2 +- .../app/KiloAgentBehaviorServiceTest.kt | 27 +++++++++++++++ .../settings/agents/SkillsSettingsUiTest.kt | 33 ++++++++++++++++++- .../settings/base/SettingsListViewTest.kt | 20 +++++++++++ 9 files changed, 138 insertions(+), 7 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index 1a85c74c37..64ca374d2c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -103,6 +103,10 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = LOG.warn("Skill save rejected: not a skill file dir=$directory path=$path") return false } + if (!knownSkill(directory, path)) { + LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path") + return false + } withContext(Dispatchers.IO) { Files.writeString(path, content, StandardCharsets.UTF_8) } @@ -241,6 +245,22 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return true } + private suspend fun knownSkill(directory: String, path: Path): Boolean { + val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + return items.any { item -> editable(item) && skillPath(item.location) == path } + } + + private fun skillPath(location: String): Path? { + val raw = normalizeWorkspacePath(location) ?: return null + val path = try { + Path.of(raw).normalize() + } catch (_: InvalidPathException) { + return null + } + if (!path.isAbsolute || !isSkillFile(path)) return null + return path + } + private fun urlCached(path: Path): Boolean { val root = Path.of(cacheRoot(), "kilo", "skills").normalize() if (path.startsWith(root)) return true diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index fad12e7cfb..5cfe2c7b4b 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -139,6 +139,9 @@ class KiloAgentBehaviorRpcApiImplTest { val dir = Files.createTempDirectory("kilo-skill-test") val file = dir.resolve("test.md") Files.writeString(file, "old") + mock.skills = """[ + {"name":"test","description":"Test","location":"$file","content":"old"} + ]""".trimIndent() val rpc = rpc() assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) @@ -151,6 +154,9 @@ class KiloAgentBehaviorRpcApiImplTest { val dir = Files.createTempDirectory("kilo-skill-test") val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") Files.writeString(file, "old") + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"$file","content":"old"} + ]""".trimIndent() val rpc = rpc() assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) @@ -160,6 +166,22 @@ class KiloAgentBehaviorRpcApiImplTest { assertFalse(rpc.saveSkill("/test project", "builtin", "nope")) } + @Test + fun `save skill rejects unknown absolute skill files`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val known = Files.createDirectories(dir.resolve("known")).resolve("SKILL.md") + val other = Files.createDirectories(dir.resolve("other")).resolve("SKILL.md") + Files.writeString(known, "known") + Files.writeString(other, "old") + mock.skills = """[ + {"name":"known","description":"Known","location":"$known","content":"known"} + ]""".trimIndent() + + assertFalse(rpc().saveSkill("/test project", other.toString(), "new content")) + + assertEquals("old", Files.readString(other)) + } + @Test fun `reload skills is blocked by pending permissions`() = runBlocking { mock.pendingPermissions = """[ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt index 1afdffea42..682df823ae 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt @@ -33,7 +33,11 @@ class KiloAgentBehaviorService internal constructor( suspend fun agents(directory: String): List = safe(emptyList()) { call { agents(directory) } } - suspend fun skills(directory: String): List = call { skills(directory) } + suspend fun skills(directory: String): List = safe(emptyList()) { call { skills(directory) } } + + suspend fun loadSkills(directory: String): List = call { skills(directory) } + + suspend fun refreshSkills(directory: String, fallback: List): List = safe(fallback) { call { skills(directory) } } suspend fun commands(directory: String): List = safe(emptyList()) { call { commands(directory) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index 164434f6de..a23a0f766a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -112,7 +112,7 @@ internal class SkillsSettingsUi( override suspend fun fetch(): List { val items = withTimeoutOrNull(SKILL_LOAD_TIMEOUT_MS) { - service().skills(dir) + service().loadSkills(dir) } ?: throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.load.timeout")) withContext(edt) { val dirty = state.modified() @@ -159,6 +159,7 @@ internal class SkillsSettingsUi( override fun applyDraft() { val token = state.start() ?: return + val fallback = skillFallback(token.target) if (!launch("apply") { id -> val target = token.target var failed: String? = null @@ -182,7 +183,8 @@ internal class SkillsSettingsUi( val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = target.sources.paths, urls = target.sources.urls)) if (app.updateConfig(patch) == null) failed = KiloBundle.message("settings.agentBehavior.save.failed") } - val items = behavior.skills(dir) + val reloaded = if (failed == null) behavior.reloadSkills(dir) else true + val items = behavior.refreshSkills(dir, fallback) withContext(edt) { if (!active(id)) { if (failed == null) KiloNotifications.info(KiloBundle.message("settings.agentBehavior.skills.saved.notification")) @@ -195,7 +197,7 @@ internal class SkillsSettingsUi( state.complete(token, next) sources.refresh(draft.sources) view.update(rows(items)) - clearProgress() + if (reloaded) clearProgress() else showProgress(KiloBundle.message("settings.agentBehavior.skills.reload.blocked")) LOG.info("skills settings apply succeeded dir=$dir") } else { state.fail(token, failed) @@ -221,6 +223,11 @@ internal class SkillsSettingsUi( item(skill) } + private fun skillFallback(target: SkillsDraft): List = skills.values.mapNotNull { skill -> + if (skill.location in target.deleted) return@mapNotNull null + target.edited[skill.location]?.let { skill.copy(content = it) } ?: skill + } + private fun item(skill: SkillDto) = object : SettingsListItem { override val key = key(skill) override val title = skill.name diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt index 18f5f3e3d4..183a86a9df 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt @@ -224,7 +224,7 @@ internal class SettingsListView( val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null if (!bounds.contains(e.point)) return null val item = model.getElementAt(idx) - val selected = idx == list.selectedIndex + val selected = list.isSelectedIndex(idx) val id = if (enabled) { settingsListCellAt(list, idx, e.point, selected) } else { 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 066a8da227..54fe7768fe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -444,7 +444,7 @@ settings.agentBehavior.skills.empty=No skills found. settings.agentBehavior.skills.content.empty=No skill content available. settings.agentBehavior.skills.load.timeout=Skill loading timed out. Existing skills were kept; remove slow or unreachable URLs and refresh. settings.agentBehavior.skills.reload.deferred=Skills source saved. Reload the core after active sessions finish to apply new skills. -settings.agentBehavior.skills.reload.blocked=Skills source saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills. +settings.agentBehavior.skills.reload.blocked=Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills. settings.agentBehavior.skills.saved.notification=Skills settings saved settings.agentBehavior.skills.delete.title=Delete Skill settings.agentBehavior.skills.delete.message=Delete skill {0}? This removes the skill file and cannot be undone. diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt index 5dd31cb642..6e69cec88d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.app import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi import ai.kilocode.rpc.dto.AgentCreateDto import ai.kilocode.rpc.dto.McpStatusDto +import ai.kilocode.rpc.dto.SkillDto import com.intellij.testFramework.fixtures.BasePlatformTestCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -10,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlin.test.assertFailsWith class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { private lateinit var scope: CoroutineScope @@ -67,6 +69,31 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { assertTrue(rpc.removals.isEmpty()) } + fun `test skills returns fallback on rpc failure`() = runBlocking { + rpc.skillsError = RuntimeException("boom") + + val items = withContext(Dispatchers.Default) { service.skills("/test") } + + assertTrue(items.isEmpty()) + } + + fun `test loadSkills propagates rpc failure`() = runBlocking { + rpc.skillsError = RuntimeException("boom") + + assertFailsWith { + withContext(Dispatchers.Default) { service.loadSkills("/test") } + } + } + + fun `test refreshSkills returns previous rows on rpc failure`() = runBlocking { + val fallback = listOf(SkillDto("plan", location = "/test/SKILL.md")) + rpc.skillsError = RuntimeException("boom") + + val items = withContext(Dispatchers.Default) { service.refreshSkills("/test", fallback) } + + assertEquals(fallback, items) + } + fun `test mcpStatus forwards directory`() = runBlocking { rpc.mcps = listOf(McpStatusDto("filesystem", "connected")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt index fbde73c460..b335631ea5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -306,7 +306,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"), edt { sourceRows(panel).map { it.key } }, ) - assertTrue(agentRpc.skillReloads.isEmpty()) + assertEquals(listOf(DIR), agentRpc.skillReloads) } fun `test stale config update result keeps added skill sources visible`() { @@ -333,6 +333,36 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { ) } + fun `test blocked reload completes apply with warning`() { + val path = "/extra/skills" + val panel = panel(choose = { path }) + agentRpc.reloadSkillResult = false + flushUntil { rows(panel).size == 3 } + + edt { + panel.sources.addPath() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertEquals(listOf(DIR), agentRpc.skillReloads) + assertEquals("Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.", edt { progressText(panel) }) + } + + fun `test post apply skills refresh failure keeps saved rows`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") }) + flushUntil { rows(panel).size == 3 } + + doubleClick(skillsList(panel), panel, CUSTOM) + agentRpc.skillsError = RuntimeException("timeout") + edt { panel.applyDraft(); true } + + flushUntil { agentRpc.skillSaves.size == 1 && !edt { panel.modified() } } + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) + assertEquals("# Saved", agentRpc.skills.single { it.location == CUSTOM }.content) + } + fun `test source reset discards staged changes`() { val path = "/extra/skills" val panel = panel(choose = { path }) @@ -367,6 +397,7 @@ class SkillsSettingsUiTest : BasePlatformTestCase() { assertEquals(emptyList(), patch.paths) assertEquals(listOf("https://skills.test/base.json"), patch.urls) assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) + assertEquals(listOf(DIR), agentRpc.skillReloads) } fun `test stale config update result keeps removed skill sources hidden`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index 46c9c1399f..d7f79d4ddf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -14,6 +14,7 @@ import java.awt.Dimension import java.awt.Point import java.awt.event.InputEvent import java.awt.event.MouseEvent +import javax.swing.ListSelectionModel import javax.swing.Scrollable import javax.swing.SwingConstants import javax.swing.SwingUtilities @@ -238,6 +239,25 @@ class SettingsListViewTest : BasePlatformTestCase() { } } + fun `test action click invokes on second selected row in multi selection list`() { + edt { + val calls = mutableListOf() + val cfg = SettingsListConfig.Equal.copy(selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) + val view = SettingsListView("Empty", cfg) { key, id -> calls += "$key:$id" } + view.update(listOf( + item("a", "Alpha", null, SettingsListCell("edit", "Edit", alwaysVisible = false)), + item("b", "Beta", null, SettingsListCell("edit", "Edit", alwaysVisible = false)), + )) + layout(view) + view.list.selectedIndices = intArrayOf(0, 1) + + val area = settingsListCellBounds(view.list, 1, selected = true).getValue("edit") + click(view, center(area)) + + assertEquals(listOf("b:edit"), calls) + } + } + fun `test update selects preferred key`() { edt { val view = SettingsListView("Empty") { _, _ -> } From 88930fe392729b8f3f78b0d6c9dc4800e0253df8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 14:06:12 -0400 Subject: [PATCH 6/9] fix(jetbrains): narrow skill cache detection --- .../ai/kilocode/backend/cli/KiloCliDataParser.kt | 1 - .../backend/rpc/KiloAgentBehaviorRpcApiImpl.kt | 16 ++++++++++------ .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 73fd3058db..1a8256393b 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -630,7 +630,6 @@ object KiloCliDataParser { description = obj.str("description"), location = location, content = obj.str("content"), - editable = obj.bool("editable"), ) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index 64ca374d2c..5b49d77699 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -262,10 +262,16 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = } private fun urlCached(path: Path): Boolean { - val root = Path.of(cacheRoot(), "kilo", "skills").normalize() - if (path.startsWith(root)) return true - val parts = (0 until path.nameCount).map { path.getName(it).toString() } - return parts.windowed(3).any { it[1] == "kilo" && it[2] == "skills" && it[0] in cacheNames } + return cacheRoots().any { root -> path.startsWith(root.resolve("kilo").resolve("skills").normalize()) } + } + + private fun cacheRoots(): Set = buildSet { + val home = System.getProperty("user.home") + add(Path.of(cacheRoot()).normalize()) + add(Path.of(home, ".cache").normalize()) + add(Path.of(home, "Library", "Caches").normalize()) + System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }?.let { add(Path.of(it).normalize()) } + add(Path.of(home, "AppData", "Local").normalize()) } private fun cacheRoot(): String { @@ -386,5 +392,3 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = val config: McpConfigDto?, ) } - -private val cacheNames = setOf(".cache", "cache", "Caches") diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index 5cfe2c7b4b..8345af98a0 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -134,6 +134,20 @@ class KiloAgentBehaviorRpcApiImplTest { assertEquals("# Remote", skill.content) } + @Test + fun `custom skills under non cache paths remain editable`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("cache/kilo/skills/custom")).resolve("SKILL.md") + Files.writeString(file, "# Custom") + mock.skills = """[ + {"name":"custom","description":"Custom","location":"$file","content":"# Custom"} + ]""".trimIndent() + + val skill = rpc().skills("/test project").single() + + assertEquals(true, skill.editable) + } + @Test fun `save skill supports configured markdown text and html files without reload`() = runBlocking { val dir = Files.createTempDirectory("kilo-skill-test") From 8187888135c9cd8e0c063e62c785128d6a810f3f Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 14:19:46 -0400 Subject: [PATCH 7/9] fix(jetbrains): batch skill saves --- .../rpc/KiloAgentBehaviorRpcApiImpl.kt | 77 ++++++++++--------- .../rpc/KiloAgentBehaviorRpcApiImplTest.kt | 21 +++++ .../client/app/KiloAgentBehaviorService.kt | 6 +- .../settings/agents/SkillsConfigurable.kt | 7 +- .../app/KiloAgentBehaviorServiceTest.kt | 20 +++-- .../client/testing/FakeAgentBehaviorRpcApi.kt | 10 +++ .../kilocode/rpc/KiloAgentBehaviorRpcApi.kt | 2 + 7 files changed, 91 insertions(+), 52 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index 5b49d77699..95185f2f20 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -89,24 +89,8 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = override suspend fun saveSkill(directory: String, location: String, content: String): Boolean { LOG.info("Skill save requested dir=$directory location=$location") app.requireReady() - val raw = normalizeWorkspacePath(location) ?: run { - LOG.warn("Skill save rejected: invalid location dir=$directory location=$location") - return false - } - val path = try { - Path.of(raw).normalize() - } catch (err: InvalidPathException) { - LOG.warn("Skill save rejected: invalid path dir=$directory location=$location", err) - return false - } - if (!path.isAbsolute || !isSkillFile(path)) { - LOG.warn("Skill save rejected: not a skill file dir=$directory path=$path") - return false - } - if (!knownSkill(directory, path)) { - LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path") - return false - } + val paths = knownSkills(directory) + val path = writablePath(directory, location, paths) ?: return false withContext(Dispatchers.IO) { Files.writeString(path, content, StandardCharsets.UTF_8) } @@ -115,6 +99,22 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return true } + override suspend fun saveSkills(directory: String, edits: Map): Boolean { + LOG.info("Skills save requested dir=$directory count=${edits.size}") + app.requireReady() + val known = knownSkills(directory) + val paths = edits.mapNotNull { (location, content) -> + val path = writablePath(directory, location, known) ?: return false + path to content + } + withContext(Dispatchers.IO) { + for ((path, content) in paths) Files.writeString(path, content, StandardCharsets.UTF_8) + } + LOG.info("Skill files saved dir=$directory count=${paths.size}") + LOG.info("Skills save reload deferred dir=$directory count=${paths.size}") + return true + } + override suspend fun removeAgent(directory: String, name: String): Boolean = post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name)))) @@ -217,13 +217,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = } private suspend fun skillContent(skill: SkillDto): String? { - val raw = normalizeWorkspacePath(skill.location) ?: return null - val path = try { - Path.of(raw).normalize() - } catch (_: InvalidPathException) { - return null - } - if (!path.isAbsolute || !isSkillFile(path)) return null + val path = resolveSkillPath(skill.location) ?: return null return runCatching { withContext(Dispatchers.IO) { if (!Files.isRegularFile(path)) null else Files.readString(path, StandardCharsets.UTF_8) @@ -234,23 +228,36 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = } private fun editable(skill: SkillDto): Boolean { - val raw = normalizeWorkspacePath(skill.location) ?: return false - val path = try { - Path.of(raw).normalize() - } catch (_: InvalidPathException) { - return false - } - if (!path.isAbsolute || !isSkillFile(path)) return false + val path = resolveSkillPath(skill.location) ?: return false if (urlCached(path)) return false return true } - private suspend fun knownSkill(directory: String, path: Path): Boolean { + private suspend fun knownSkills(directory: String): Set { val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) - return items.any { item -> editable(item) && skillPath(item.location) == path } + return items.mapNotNull { item -> resolveEditablePath(item) }.toSet() } - private fun skillPath(location: String): Path? { + private fun writablePath(directory: String, location: String, known: Set): Path? { + val path = resolveSkillPath(location) + if (path == null) { + LOG.warn("Skill save rejected: invalid location dir=$directory location=$location") + return null + } + if (path !in known) { + LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path") + return null + } + return path + } + + private fun resolveEditablePath(skill: SkillDto): Path? { + val path = resolveSkillPath(skill.location) ?: return null + if (urlCached(path)) return null + return path + } + + private fun resolveSkillPath(location: String): Path? { val raw = normalizeWorkspacePath(location) ?: return null val path = try { Path.of(raw).normalize() diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index 8345af98a0..880e6cba4c 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -180,6 +180,27 @@ class KiloAgentBehaviorRpcApiImplTest { assertFalse(rpc.saveSkill("/test project", "builtin", "nope")) } + @Test + fun `save skills validates known paths once for multiple edits`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val plan = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + val review = Files.createDirectories(dir.resolve("review")).resolve("SKILL.md") + Files.writeString(plan, "old plan") + Files.writeString(review, "old review") + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"$plan","content":"old plan"}, + {"name":"review","description":"Review work","location":"$review","content":"old review"} + ]""".trimIndent() + val rpc = rpc() + mock.resetCounts() + + assertTrue(rpc.saveSkills("/test project", mapOf(plan.toString() to "new plan", review.toString() to "new review"))) + + assertEquals("new plan", Files.readString(plan)) + assertEquals("new review", Files.readString(review)) + assertEquals(1, mock.requestCount("/skill")) + } + @Test fun `save skill rejects unknown absolute skill files`() = runBlocking { val dir = Files.createTempDirectory("kilo-skill-test") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt index 682df823ae..2171cb1094 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt @@ -33,8 +33,6 @@ class KiloAgentBehaviorService internal constructor( suspend fun agents(directory: String): List = safe(emptyList()) { call { agents(directory) } } - suspend fun skills(directory: String): List = safe(emptyList()) { call { skills(directory) } } - suspend fun loadSkills(directory: String): List = call { skills(directory) } suspend fun refreshSkills(directory: String, fallback: List): List = safe(fallback) { call { skills(directory) } } @@ -58,8 +56,8 @@ class KiloAgentBehaviorService internal constructor( suspend fun reloadSkills(directory: String): Boolean = safe(false) { call { reloadSkills(directory) } } - suspend fun saveSkill(directory: String, location: String, content: String): Boolean = - safe(false) { call { saveSkill(directory, location, content) } } + suspend fun saveSkills(directory: String, edits: Map): Boolean = + safe(false) { call { saveSkills(directory, edits) } } suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt index a23a0f766a..03ce991d73 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -165,11 +165,8 @@ internal class SkillsSettingsUi( var failed: String? = null val behavior = service() LOG.info("skills settings apply start dir=$dir edited=${target.edited.size} deleted=${target.deleted.size} paths=${target.sources.paths.size} urls=${target.sources.urls.size}") - for ((location, content) in target.edited) { - if (!behavior.saveSkill(dir, location, content)) { - failed = KiloBundle.message("settings.agentBehavior.save.failed") - break - } + if (target.edited.isNotEmpty() && !behavior.saveSkills(dir, target.edited)) { + failed = KiloBundle.message("settings.agentBehavior.save.failed") } if (failed == null) { for (location in target.deleted) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt index 6e69cec88d..fefcde20dd 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt @@ -69,14 +69,6 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { assertTrue(rpc.removals.isEmpty()) } - fun `test skills returns fallback on rpc failure`() = runBlocking { - rpc.skillsError = RuntimeException("boom") - - val items = withContext(Dispatchers.Default) { service.skills("/test") } - - assertTrue(items.isEmpty()) - } - fun `test loadSkills propagates rpc failure`() = runBlocking { rpc.skillsError = RuntimeException("boom") @@ -94,6 +86,18 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { assertEquals(fallback, items) } + fun `test saveSkills forwards all edits`() = runBlocking { + rpc.skills = listOf(SkillDto("plan", location = "/test/plan/SKILL.md")) + + val ok = withContext(Dispatchers.Default) { + service.saveSkills("/test", mapOf("/test/plan/SKILL.md" to "# Saved")) + } + + assertTrue(ok) + assertEquals(listOf(Triple("/test", "/test/plan/SKILL.md", "# Saved")), rpc.skillSaves) + assertEquals("# Saved", rpc.skills.single().content) + } + fun `test mcpStatus forwards directory`() = runBlocking { rpc.mcps = listOf(McpStatusDto("filesystem", "connected")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt index 8b69e89a68..1d5d5c5afb 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt @@ -81,6 +81,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { return saveSkillResult } + override suspend fun saveSkills(directory: String, edits: Map): Boolean { + assertNotEdt("agentBehavior.saveSkills") + saveSkillError?.let { throw it } + for ((location, content) in edits) skillSaves.add(Triple(directory, location, content)) + if (saveSkillResult) skills = skills.map { skill -> + edits[skill.location]?.let { skill.copy(content = it) } ?: skill + } + return saveSkillResult + } + override suspend fun removeAgent(directory: String, name: String): Boolean { assertNotEdt("agentBehavior.removeAgent") removeError?.let { throw it } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt index eaa2223045..b13c03d526 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt @@ -30,6 +30,8 @@ interface KiloAgentBehaviorRpcApi : RemoteApi { suspend fun saveSkill(directory: String, location: String, content: String): Boolean + suspend fun saveSkills(directory: String, edits: Map): Boolean + suspend fun removeAgent(directory: String, name: String): Boolean suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean From c564d1c232a86c7fb439464bff7b3ef947965322 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 14:35:54 -0400 Subject: [PATCH 8/9] fix(jetbrains): translate skills settings --- .../messages/KiloBundle_ar.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_bs.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_da.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_de.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_es.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_fr.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_ja.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_ko.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_nl.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_no.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_pl.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_pt_BR.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_ru.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_th.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_tr.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_uk.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_zh_CN.properties | 27 +++++++++++++++++++ .../messages/KiloBundle_zh_TW.properties | 27 +++++++++++++++++++ 18 files changed, 486 insertions(+) diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index b2dcd3e748..883417cf58 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -1,4 +1,6 @@ common.delete=حذف +common.open=فتح +common.save=حفظ session.action.cancel=إلغاء session.connection.connecting=جاري التحميل… session.connection.error.app=فشل الاتصال @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته. +settings.agentBehavior.skills.displayName=المهارات +settings.agentBehavior.skills.search=تصفية المهارات +settings.agentBehavior.skills.empty=لم يتم العثور على مهارات. +settings.agentBehavior.skills.content.empty=لا يوجد محتوى مهارة متاح. +settings.agentBehavior.skills.load.timeout=انتهت مهلة تحميل المهارات. تم الاحتفاظ بالمهارات الحالية؛ أزل عناوين URL البطيئة أو غير المتاحة ثم حدّث. +settings.agentBehavior.skills.reload.deferred=تم حفظ مصدر المهارات. أعد تحميل Core بعد انتهاء الجلسات النشطة لتطبيق المهارات الجديدة. +settings.agentBehavior.skills.reload.blocked=تم حفظ إعدادات المهارات، لكن توجد جلسات نشطة. أعد تحميل Core بعد انتهاء تلك الجلسات لتطبيق المهارات الجديدة. +settings.agentBehavior.skills.saved.notification=تم حفظ إعدادات المهارات +settings.agentBehavior.skills.delete.title=حذف المهارة +settings.agentBehavior.skills.delete.message=هل تريد حذف المهارة {0}؟ سيؤدي ذلك إلى إزالة ملف المهارة ولا يمكن التراجع عنه. +settings.agentBehavior.skills.delete.failed=تعذر حذف المهارة. +settings.agentBehavior.skills.openInEditor=فتح في المحرر +settings.agentBehavior.skills.openInEditor.pending=سيتم فتح ملف المهارة بعد إغلاق الإعدادات. +settings.agentBehavior.skills.openInEditor.failed=تعذر فتح ملف المهارة في المحرر. +settings.agentBehavior.skills.sources.empty=لم يتم تكوين مصادر مهارات. +settings.agentBehavior.skills.sources.title=مصادر مهارات إضافية +settings.agentBehavior.skills.sources.add=إضافة +settings.agentBehavior.skills.sources.addPath=إضافة مسار +settings.agentBehavior.skills.sources.addPath.title=إضافة مسار مهارات +settings.agentBehavior.skills.sources.addPath.prompt=اختر مجلداً يحتوي على مهارات Kilo. +settings.agentBehavior.skills.sources.addUrl=إضافة URL +settings.agentBehavior.skills.sources.addUrl.title=إضافة URL لمصدر مهارات +settings.agentBehavior.skills.sources.addUrl.prompt=أدخل URL لمصدر مهارات. +settings.agentBehavior.skills.sources.editPath.title=تعديل مسار المهارات +settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=تم التراجع عن رسالة واحدة diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index 85a36ca7b9..4a73d8a174 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -1,4 +1,6 @@ common.delete=Obriši +common.open=Otvori +common.save=Sačuvaj session.action.cancel=Otkaži session.connection.connecting=Učitavanje… session.connection.error.app=Greška pri spajanju @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda. +settings.agentBehavior.skills.displayName=Vještine +settings.agentBehavior.skills.search=Filtriraj vještine +settings.agentBehavior.skills.empty=Nema pronađenih vještina. +settings.agentBehavior.skills.content.empty=Nema dostupnog sadržaja vještine. +settings.agentBehavior.skills.load.timeout=Učitavanje vještina je isteklo. Postojeće vještine su zadržane; uklonite spore ili nedostupne URL-ove i osvježite. +settings.agentBehavior.skills.reload.deferred=Izvor vještina je sačuvan. Ponovo učitajte Core nakon što aktivne sesije završe da primijenite nove vještine. +settings.agentBehavior.skills.reload.blocked=Postavke vještina su sačuvane, ali postoje aktivne sesije. Ponovo učitajte Core nakon što te sesije završe da primijenite nove vještine. +settings.agentBehavior.skills.saved.notification=Postavke vještina su sačuvane +settings.agentBehavior.skills.delete.title=Izbriši vještinu +settings.agentBehavior.skills.delete.message=Izbrisati vještinu {0}? Ovo uklanja datoteku vještine i ne može se poništiti. +settings.agentBehavior.skills.delete.failed=Nije moguće izbrisati vještinu. +settings.agentBehavior.skills.openInEditor=Otvori u editoru +settings.agentBehavior.skills.openInEditor.pending=Datoteka vještine će se otvoriti nakon što zatvorite Postavke. +settings.agentBehavior.skills.openInEditor.failed=Nije moguće otvoriti datoteku vještine u editoru. +settings.agentBehavior.skills.sources.empty=Nema konfigurisanih izvora vještina. +settings.agentBehavior.skills.sources.title=Dodatni izvori vještina +settings.agentBehavior.skills.sources.add=Dodaj +settings.agentBehavior.skills.sources.addPath=Dodaj putanju +settings.agentBehavior.skills.sources.addPath.title=Dodaj putanju vještina +settings.agentBehavior.skills.sources.addPath.prompt=Odaberite folder koji sadrži Kilo vještine. +settings.agentBehavior.skills.sources.addUrl=Dodaj URL +settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL izvora vještina +settings.agentBehavior.skills.sources.addUrl.prompt=Unesite URL izvora vještina. +settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina +settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} poruka vraćena diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 5247615f46..6af8961f05 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -1,4 +1,6 @@ common.delete=Slet +common.open=Åbn +common.save=Gem session.action.cancel=Annuller session.connection.connecting=Indlæser… session.connection.error.app=Forbindelsesfejl @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=For at tilføje en MCP-server skal du bede agenten om at gøre det. +settings.agentBehavior.skills.displayName=Færdigheder +settings.agentBehavior.skills.search=Filtrer færdigheder +settings.agentBehavior.skills.empty=Ingen færdigheder fundet. +settings.agentBehavior.skills.content.empty=Intet færdighedsindhold tilgængeligt. +settings.agentBehavior.skills.load.timeout=Indlæsning af færdigheder fik timeout. Eksisterende færdigheder blev bevaret; fjern langsomme eller utilgængelige URL'er og opdater. +settings.agentBehavior.skills.reload.deferred=Færdighedskilden blev gemt. Genindlæs Core, når aktive sessioner er afsluttet, for at anvende nye færdigheder. +settings.agentBehavior.skills.reload.blocked=Færdighedsindstillingerne blev gemt, men der er aktive sessioner. Genindlæs Core, når disse sessioner er afsluttet, for at anvende de nye færdigheder. +settings.agentBehavior.skills.saved.notification=Færdighedsindstillinger gemt +settings.agentBehavior.skills.delete.title=Slet færdighed +settings.agentBehavior.skills.delete.message=Slet færdigheden {0}? Dette fjerner færdighedsfilen og kan ikke fortrydes. +settings.agentBehavior.skills.delete.failed=Kunne ikke slette færdigheden. +settings.agentBehavior.skills.openInEditor=Åbn i editor +settings.agentBehavior.skills.openInEditor.pending=Færdighedsfilen åbnes, når du lukker Indstillinger. +settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åbne færdighedsfilen i editoren. +settings.agentBehavior.skills.sources.empty=Ingen færdighedskilder konfigureret. +settings.agentBehavior.skills.sources.title=Yderligere færdighedskilder +settings.agentBehavior.skills.sources.add=Tilføj +settings.agentBehavior.skills.sources.addPath=Tilføj sti +settings.agentBehavior.skills.sources.addPath.title=Tilføj færdighedssti +settings.agentBehavior.skills.sources.addPath.prompt=Vælg en mappe, der indeholder Kilo-færdigheder. +settings.agentBehavior.skills.sources.addUrl=Tilføj URL +settings.agentBehavior.skills.sources.addUrl.title=Tilføj URL til færdighedskilde +settings.agentBehavior.skills.sources.addUrl.prompt=Indtast en URL til en færdighedskilde. +settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti +settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} besked rullet tilbage diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 7a3434f0b2..d85a6353f8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -1,4 +1,6 @@ common.delete=Löschen +common.open=Öffnen +common.save=Speichern session.action.cancel=Abbrechen session.connection.connecting=Wird geladen… session.connection.error.app=Verbindung fehlgeschlagen @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Um einen MCP-Server hinzuzufügen, bitten Sie den Agenten darum. +settings.agentBehavior.skills.displayName=Skills +settings.agentBehavior.skills.search=Skills filtern +settings.agentBehavior.skills.empty=Keine Skills gefunden. +settings.agentBehavior.skills.content.empty=Keine Skill-Inhalte verfügbar. +settings.agentBehavior.skills.load.timeout=Das Laden der Skills ist abgelaufen. Vorhandene Skills wurden beibehalten; entfernen Sie langsame oder nicht erreichbare URLs und aktualisieren Sie. +settings.agentBehavior.skills.reload.deferred=Skill-Quelle gespeichert. Laden Sie Core neu, nachdem aktive Sitzungen beendet sind, um neue Skills anzuwenden. +settings.agentBehavior.skills.reload.blocked=Skill-Einstellungen gespeichert, aber es sind aktive Sitzungen vorhanden. Laden Sie Core neu, nachdem diese Sitzungen beendet sind, um neue Skills anzuwenden. +settings.agentBehavior.skills.saved.notification=Skill-Einstellungen gespeichert +settings.agentBehavior.skills.delete.title=Skill löschen +settings.agentBehavior.skills.delete.message=Skill {0} löschen? Dadurch wird die Skill-Datei entfernt und kann nicht rückgängig gemacht werden. +settings.agentBehavior.skills.delete.failed=Der Skill konnte nicht gelöscht werden. +settings.agentBehavior.skills.openInEditor=Im Editor öffnen +settings.agentBehavior.skills.openInEditor.pending=Die Skill-Datei wird geöffnet, nachdem Sie die Einstellungen geschlossen haben. +settings.agentBehavior.skills.openInEditor.failed=Die Skill-Datei konnte nicht im Editor geöffnet werden. +settings.agentBehavior.skills.sources.empty=Keine Skill-Quellen konfiguriert. +settings.agentBehavior.skills.sources.title=Zusätzliche Skill-Quellen +settings.agentBehavior.skills.sources.add=Hinzufügen +settings.agentBehavior.skills.sources.addPath=Pfad hinzufügen +settings.agentBehavior.skills.sources.addPath.title=Skill-Pfad hinzufügen +settings.agentBehavior.skills.sources.addPath.prompt=Wählen Sie einen Ordner mit Kilo-Skills aus. +settings.agentBehavior.skills.sources.addUrl=URL hinzufügen +settings.agentBehavior.skills.sources.addUrl.title=Skill-Quellen-URL hinzufügen +settings.agentBehavior.skills.sources.addUrl.prompt=Geben Sie eine Skill-Quellen-URL ein. +settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten +settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} Nachricht zurückgesetzt diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index ced3ac3fc4..06a9f283ec 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -1,4 +1,6 @@ common.delete=Eliminar +common.open=Abrir +common.save=Guardar session.action.cancel=Cancelar session.connection.connecting=Cargando… session.connection.error.app=Error de conexión @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Para agregar un servidor MCP, pídele al agente que lo haga. +settings.agentBehavior.skills.displayName=Habilidades +settings.agentBehavior.skills.search=Filtrar habilidades +settings.agentBehavior.skills.empty=No se encontraron habilidades. +settings.agentBehavior.skills.content.empty=No hay contenido de habilidad disponible. +settings.agentBehavior.skills.load.timeout=Se agotó el tiempo de carga de habilidades. Se conservaron las habilidades existentes; elimina las URL lentas o inaccesibles y actualiza. +settings.agentBehavior.skills.reload.deferred=Fuente de habilidades guardada. Recarga Core cuando terminen las sesiones activas para aplicar nuevas habilidades. +settings.agentBehavior.skills.reload.blocked=Configuración de habilidades guardada, pero hay sesiones activas. Recarga Core cuando esas sesiones terminen para aplicar las nuevas habilidades. +settings.agentBehavior.skills.saved.notification=Configuración de habilidades guardada +settings.agentBehavior.skills.delete.title=Eliminar habilidad +settings.agentBehavior.skills.delete.message=¿Eliminar la habilidad {0}? Esto elimina el archivo de la habilidad y no se puede deshacer. +settings.agentBehavior.skills.delete.failed=No se pudo eliminar la habilidad. +settings.agentBehavior.skills.openInEditor=Abrir en el editor +settings.agentBehavior.skills.openInEditor.pending=El archivo de la habilidad se abrirá después de cerrar Configuración. +settings.agentBehavior.skills.openInEditor.failed=No se pudo abrir el archivo de la habilidad en el editor. +settings.agentBehavior.skills.sources.empty=No hay fuentes de habilidades configuradas. +settings.agentBehavior.skills.sources.title=Fuentes de habilidades adicionales +settings.agentBehavior.skills.sources.add=Agregar +settings.agentBehavior.skills.sources.addPath=Agregar ruta +settings.agentBehavior.skills.sources.addPath.title=Agregar ruta de habilidades +settings.agentBehavior.skills.sources.addPath.prompt=Elige una carpeta que contenga habilidades de Kilo. +settings.agentBehavior.skills.sources.addUrl=Agregar URL +settings.agentBehavior.skills.sources.addUrl.title=Agregar URL de fuente de habilidades +settings.agentBehavior.skills.sources.addUrl.prompt=Introduce una URL de fuente de habilidades. +settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades +settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mensaje revertido diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index 9fe242ab46..6198c80420 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -1,4 +1,6 @@ common.delete=Supprimer +common.open=Ouvrir +common.save=Enregistrer session.action.cancel=Annuler session.connection.connecting=Chargement… session.connection.error.app=Échec de la connexion @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à l’agent de le faire. +settings.agentBehavior.skills.displayName=Compétences +settings.agentBehavior.skills.search=Filtrer les compétences +settings.agentBehavior.skills.empty=Aucune compétence trouvée. +settings.agentBehavior.skills.content.empty=Aucun contenu de compétence disponible. +settings.agentBehavior.skills.load.timeout=Le chargement des compétences a expiré. Les compétences existantes ont été conservées ; supprimez les URL lentes ou inaccessibles puis actualisez. +settings.agentBehavior.skills.reload.deferred=Source de compétences enregistrée. Rechargez Core après la fin des sessions actives pour appliquer les nouvelles compétences. +settings.agentBehavior.skills.reload.blocked=Paramètres des compétences enregistrés, mais des sessions sont actives. Rechargez Core après la fin de ces sessions pour appliquer les nouvelles compétences. +settings.agentBehavior.skills.saved.notification=Paramètres des compétences enregistrés +settings.agentBehavior.skills.delete.title=Supprimer la compétence +settings.agentBehavior.skills.delete.message=Supprimer la compétence {0} ? Cela supprime le fichier de compétence et ne peut pas être annulé. +settings.agentBehavior.skills.delete.failed=Impossible de supprimer la compétence. +settings.agentBehavior.skills.openInEditor=Ouvrir dans l’éditeur +settings.agentBehavior.skills.openInEditor.pending=Le fichier de compétence s’ouvrira après la fermeture des paramètres. +settings.agentBehavior.skills.openInEditor.failed=Impossible d’ouvrir le fichier de compétence dans l’éditeur. +settings.agentBehavior.skills.sources.empty=Aucune source de compétences configurée. +settings.agentBehavior.skills.sources.title=Sources de compétences supplémentaires +settings.agentBehavior.skills.sources.add=Ajouter +settings.agentBehavior.skills.sources.addPath=Ajouter un chemin +settings.agentBehavior.skills.sources.addPath.title=Ajouter un chemin de compétences +settings.agentBehavior.skills.sources.addPath.prompt=Choisissez un dossier contenant des compétences Kilo. +settings.agentBehavior.skills.sources.addUrl=Ajouter une URL +settings.agentBehavior.skills.sources.addUrl.title=Ajouter une URL de source de compétences +settings.agentBehavior.skills.sources.addUrl.prompt=Saisissez une URL de source de compétences. +settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des compétences +settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} message annulé diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 6d0a77f804..3aa20cb6c1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -1,4 +1,6 @@ common.delete=削除 +common.open=開く +common.save=保存 session.action.cancel=キャンセル session.connection.connecting=読み込み中… session.connection.error.app=接続に失敗しました @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。 +settings.agentBehavior.skills.displayName=スキル +settings.agentBehavior.skills.search=スキルを絞り込み +settings.agentBehavior.skills.empty=スキルが見つかりません。 +settings.agentBehavior.skills.content.empty=利用可能なスキル内容がありません。 +settings.agentBehavior.skills.load.timeout=スキルの読み込みがタイムアウトしました。既存のスキルは保持されました。遅い、または到達不能な URL を削除して更新してください。 +settings.agentBehavior.skills.reload.deferred=スキルソースを保存しました。新しいスキルを適用するには、アクティブなセッションが終了した後に Core を再読み込みしてください。 +settings.agentBehavior.skills.reload.blocked=スキル設定を保存しましたが、アクティブなセッションがあります。新しいスキルを適用するには、それらのセッションが終了した後に Core を再読み込みしてください。 +settings.agentBehavior.skills.saved.notification=スキル設定を保存しました +settings.agentBehavior.skills.delete.title=スキルを削除 +settings.agentBehavior.skills.delete.message=スキル {0} を削除しますか?スキルファイルが削除され、この操作は元に戻せません。 +settings.agentBehavior.skills.delete.failed=スキルを削除できませんでした。 +settings.agentBehavior.skills.openInEditor=エディターで開く +settings.agentBehavior.skills.openInEditor.pending=設定を閉じるとスキルファイルが開きます。 +settings.agentBehavior.skills.openInEditor.failed=エディターでスキルファイルを開けませんでした。 +settings.agentBehavior.skills.sources.empty=スキルソースが設定されていません。 +settings.agentBehavior.skills.sources.title=追加のスキルソース +settings.agentBehavior.skills.sources.add=追加 +settings.agentBehavior.skills.sources.addPath=パスを追加 +settings.agentBehavior.skills.sources.addPath.title=スキルパスを追加 +settings.agentBehavior.skills.sources.addPath.prompt=Kilo スキルを含むフォルダーを選択してください。 +settings.agentBehavior.skills.sources.addUrl=URL を追加 +settings.agentBehavior.skills.sources.addUrl.title=スキルソース URL を追加 +settings.agentBehavior.skills.sources.addUrl.prompt=スキルソース URL を入力してください。 +settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集 +settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} 件のメッセージをロールバックしました diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 9f085b0099..4dea6ea847 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -1,4 +1,6 @@ common.delete=삭제 +common.open=열기 +common.save=저장 session.action.cancel=취소 session.connection.connecting=로딩 중… session.connection.error.app=연결 실패 @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요. +settings.agentBehavior.skills.displayName=스킬 +settings.agentBehavior.skills.search=스킬 필터링 +settings.agentBehavior.skills.empty=스킬을 찾을 수 없습니다. +settings.agentBehavior.skills.content.empty=사용 가능한 스킬 내용이 없습니다. +settings.agentBehavior.skills.load.timeout=스킬 로드 시간이 초과되었습니다. 기존 스킬은 유지되었습니다. 느리거나 연결할 수 없는 URL을 제거한 뒤 새로 고치세요. +settings.agentBehavior.skills.reload.deferred=스킬 소스가 저장되었습니다. 새 스킬을 적용하려면 활성 세션이 끝난 뒤 Core를 다시 로드하세요. +settings.agentBehavior.skills.reload.blocked=스킬 설정이 저장되었지만 활성 세션이 있습니다. 새 스킬을 적용하려면 해당 세션이 끝난 뒤 Core를 다시 로드하세요. +settings.agentBehavior.skills.saved.notification=스킬 설정이 저장되었습니다 +settings.agentBehavior.skills.delete.title=스킬 삭제 +settings.agentBehavior.skills.delete.message=스킬 {0}을 삭제할까요? 스킬 파일이 제거되며 되돌릴 수 없습니다. +settings.agentBehavior.skills.delete.failed=스킬을 삭제할 수 없습니다. +settings.agentBehavior.skills.openInEditor=에디터에서 열기 +settings.agentBehavior.skills.openInEditor.pending=설정을 닫으면 스킬 파일이 열립니다. +settings.agentBehavior.skills.openInEditor.failed=에디터에서 스킬 파일을 열 수 없습니다. +settings.agentBehavior.skills.sources.empty=구성된 스킬 소스가 없습니다. +settings.agentBehavior.skills.sources.title=추가 스킬 소스 +settings.agentBehavior.skills.sources.add=추가 +settings.agentBehavior.skills.sources.addPath=경로 추가 +settings.agentBehavior.skills.sources.addPath.title=스킬 경로 추가 +settings.agentBehavior.skills.sources.addPath.prompt=Kilo 스킬이 포함된 폴더를 선택하세요. +settings.agentBehavior.skills.sources.addUrl=URL 추가 +settings.agentBehavior.skills.sources.addUrl.title=스킬 소스 URL 추가 +settings.agentBehavior.skills.sources.addUrl.prompt=스킬 소스 URL을 입력하세요. +settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집 +settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0}개 메시지가 롤백됨 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index 5f18710f4f..2c34ed6722 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -1,4 +1,6 @@ common.delete=Verwijderen +common.open=Openen +common.save=Opslaan session.action.cancel=Annuleren session.connection.connecting=Laden… session.connection.error.app=Verbinding mislukt @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen. +settings.agentBehavior.skills.displayName=Vaardigheden +settings.agentBehavior.skills.search=Vaardigheden filteren +settings.agentBehavior.skills.empty=Geen vaardigheden gevonden. +settings.agentBehavior.skills.content.empty=Geen vaardigheidsinhoud beschikbaar. +settings.agentBehavior.skills.load.timeout=Het laden van vaardigheden is verlopen. Bestaande vaardigheden zijn behouden; verwijder trage of onbereikbare URL’s en vernieuw. +settings.agentBehavior.skills.reload.deferred=Vaardigheidsbron opgeslagen. Laad Core opnieuw nadat actieve sessies zijn voltooid om nieuwe vaardigheden toe te passen. +settings.agentBehavior.skills.reload.blocked=Vaardigheidsinstellingen opgeslagen, maar er zijn actieve sessies. Laad Core opnieuw nadat die sessies zijn voltooid om de nieuwe vaardigheden toe te passen. +settings.agentBehavior.skills.saved.notification=Vaardigheidsinstellingen opgeslagen +settings.agentBehavior.skills.delete.title=Vaardigheid verwijderen +settings.agentBehavior.skills.delete.message=Vaardigheid {0} verwijderen? Dit verwijdert het vaardigheidsbestand en kan niet ongedaan worden gemaakt. +settings.agentBehavior.skills.delete.failed=Kon de vaardigheid niet verwijderen. +settings.agentBehavior.skills.openInEditor=Openen in editor +settings.agentBehavior.skills.openInEditor.pending=Het vaardigheidsbestand wordt geopend nadat u Instellingen sluit. +settings.agentBehavior.skills.openInEditor.failed=Kon het vaardigheidsbestand niet openen in de editor. +settings.agentBehavior.skills.sources.empty=Geen vaardigheidsbronnen geconfigureerd. +settings.agentBehavior.skills.sources.title=Extra vaardigheidsbronnen +settings.agentBehavior.skills.sources.add=Toevoegen +settings.agentBehavior.skills.sources.addPath=Pad toevoegen +settings.agentBehavior.skills.sources.addPath.title=Vaardigheidspad toevoegen +settings.agentBehavior.skills.sources.addPath.prompt=Kies een map met Kilo-vaardigheden. +settings.agentBehavior.skills.sources.addUrl=URL toevoegen +settings.agentBehavior.skills.sources.addUrl.title=URL van vaardigheidsbron toevoegen +settings.agentBehavior.skills.sources.addUrl.prompt=Voer een URL van een vaardigheidsbron in. +settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken +settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} bericht teruggedraaid diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index 1f5e95dceb..c152f5201b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -1,4 +1,6 @@ common.delete=Slett +common.open=Åpne +common.save=Lagre session.action.cancel=Avbryt session.connection.connecting=Laster… session.connection.error.app=Tilkoblingsfeil @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server. +settings.agentBehavior.skills.displayName=Ferdigheter +settings.agentBehavior.skills.search=Filtrer ferdigheter +settings.agentBehavior.skills.empty=Ingen ferdigheter funnet. +settings.agentBehavior.skills.content.empty=Ingen ferdighetsinnhold tilgjengelig. +settings.agentBehavior.skills.load.timeout=Innlasting av ferdigheter tidsavbrutt. Eksisterende ferdigheter ble beholdt; fjern trege eller utilgjengelige URL-er og oppdater. +settings.agentBehavior.skills.reload.deferred=Ferdighetskilden ble lagret. Last Core på nytt etter at aktive økter er ferdige for å bruke nye ferdigheter. +settings.agentBehavior.skills.reload.blocked=Ferdighetsinnstillinger ble lagret, men det finnes aktive økter. Last Core på nytt etter at disse øktene er ferdige for å bruke de nye ferdighetene. +settings.agentBehavior.skills.saved.notification=Ferdighetsinnstillinger lagret +settings.agentBehavior.skills.delete.title=Slett ferdighet +settings.agentBehavior.skills.delete.message=Slette ferdigheten {0}? Dette fjerner ferdighetsfilen og kan ikke angres. +settings.agentBehavior.skills.delete.failed=Kunne ikke slette ferdigheten. +settings.agentBehavior.skills.openInEditor=Åpne i editor +settings.agentBehavior.skills.openInEditor.pending=Ferdighetsfilen åpnes etter at du lukker Innstillinger. +settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åpne ferdighetsfilen i editoren. +settings.agentBehavior.skills.sources.empty=Ingen ferdighetskilder konfigurert. +settings.agentBehavior.skills.sources.title=Flere ferdighetskilder +settings.agentBehavior.skills.sources.add=Legg til +settings.agentBehavior.skills.sources.addPath=Legg til sti +settings.agentBehavior.skills.sources.addPath.title=Legg til ferdighetssti +settings.agentBehavior.skills.sources.addPath.prompt=Velg en mappe som inneholder Kilo-ferdigheter. +settings.agentBehavior.skills.sources.addUrl=Legg til URL +settings.agentBehavior.skills.sources.addUrl.title=Legg til URL for ferdighetskilde +settings.agentBehavior.skills.sources.addUrl.prompt=Skriv inn en URL for ferdighetskilde. +settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti +settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} melding rullet tilbake diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index 258f0884b2..9b11d28a1c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -1,4 +1,6 @@ common.delete=Usuń +common.open=Otwórz +common.save=Zapisz session.action.cancel=Anuluj session.connection.connecting=Ładowanie… session.connection.error.app=Błąd połączenia @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił. +settings.agentBehavior.skills.displayName=Umiejętności +settings.agentBehavior.skills.search=Filtruj umiejętności +settings.agentBehavior.skills.empty=Nie znaleziono umiejętności. +settings.agentBehavior.skills.content.empty=Brak dostępnej treści umiejętności. +settings.agentBehavior.skills.load.timeout=Przekroczono czas ładowania umiejętności. Istniejące umiejętności zostały zachowane; usuń wolne lub niedostępne adresy URL i odśwież. +settings.agentBehavior.skills.reload.deferred=Źródło umiejętności zapisane. Przeładuj Core po zakończeniu aktywnych sesji, aby zastosować nowe umiejętności. +settings.agentBehavior.skills.reload.blocked=Ustawienia umiejętności zapisane, ale są aktywne sesje. Przeładuj Core po zakończeniu tych sesji, aby zastosować nowe umiejętności. +settings.agentBehavior.skills.saved.notification=Ustawienia umiejętności zapisane +settings.agentBehavior.skills.delete.title=Usuń umiejętność +settings.agentBehavior.skills.delete.message=Usunąć umiejętność {0}? Spowoduje to usunięcie pliku umiejętności i nie można tego cofnąć. +settings.agentBehavior.skills.delete.failed=Nie można usunąć umiejętności. +settings.agentBehavior.skills.openInEditor=Otwórz w edytorze +settings.agentBehavior.skills.openInEditor.pending=Plik umiejętności otworzy się po zamknięciu Ustawień. +settings.agentBehavior.skills.openInEditor.failed=Nie można otworzyć pliku umiejętności w edytorze. +settings.agentBehavior.skills.sources.empty=Nie skonfigurowano źródeł umiejętności. +settings.agentBehavior.skills.sources.title=Dodatkowe źródła umiejętności +settings.agentBehavior.skills.sources.add=Dodaj +settings.agentBehavior.skills.sources.addPath=Dodaj ścieżkę +settings.agentBehavior.skills.sources.addPath.title=Dodaj ścieżkę umiejętności +settings.agentBehavior.skills.sources.addPath.prompt=Wybierz folder zawierający umiejętności Kilo. +settings.agentBehavior.skills.sources.addUrl=Dodaj URL +settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL źródła umiejętności +settings.agentBehavior.skills.sources.addUrl.prompt=Wpisz URL źródła umiejętności. +settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętności +settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Cofnięto {0} wiadomość diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 271591b25d..fc43bae11d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -1,4 +1,6 @@ common.delete=Excluir +common.open=Abrir +common.save=Salvar session.action.cancel=Cancelar session.connection.connecting=Carregando… session.connection.error.app=Falha na conexão @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Para adicionar um servidor MCP, peça ao agente para fazer isso. +settings.agentBehavior.skills.displayName=Habilidades +settings.agentBehavior.skills.search=Filtrar habilidades +settings.agentBehavior.skills.empty=Nenhuma habilidade encontrada. +settings.agentBehavior.skills.content.empty=Nenhum conteúdo de habilidade disponível. +settings.agentBehavior.skills.load.timeout=O carregamento de habilidades atingiu o tempo limite. As habilidades existentes foram mantidas; remova URLs lentas ou inacessíveis e atualize. +settings.agentBehavior.skills.reload.deferred=Fonte de habilidades salva. Recarregue o Core depois que as sessões ativas terminarem para aplicar novas habilidades. +settings.agentBehavior.skills.reload.blocked=Configurações de habilidades salvas, mas há sessões ativas. Recarregue o Core depois que essas sessões terminarem para aplicar as novas habilidades. +settings.agentBehavior.skills.saved.notification=Configurações de habilidades salvas +settings.agentBehavior.skills.delete.title=Excluir habilidade +settings.agentBehavior.skills.delete.message=Excluir a habilidade {0}? Isso remove o arquivo da habilidade e não pode ser desfeito. +settings.agentBehavior.skills.delete.failed=Não foi possível excluir a habilidade. +settings.agentBehavior.skills.openInEditor=Abrir no editor +settings.agentBehavior.skills.openInEditor.pending=O arquivo da habilidade será aberto depois que você fechar as Configurações. +settings.agentBehavior.skills.openInEditor.failed=Não foi possível abrir o arquivo da habilidade no editor. +settings.agentBehavior.skills.sources.empty=Nenhuma fonte de habilidades configurada. +settings.agentBehavior.skills.sources.title=Fontes de habilidades adicionais +settings.agentBehavior.skills.sources.add=Adicionar +settings.agentBehavior.skills.sources.addPath=Adicionar caminho +settings.agentBehavior.skills.sources.addPath.title=Adicionar caminho de habilidades +settings.agentBehavior.skills.sources.addPath.prompt=Escolha uma pasta contendo habilidades do Kilo. +settings.agentBehavior.skills.sources.addUrl=Adicionar URL +settings.agentBehavior.skills.sources.addUrl.title=Adicionar URL de fonte de habilidades +settings.agentBehavior.skills.sources.addUrl.prompt=Insira uma URL de fonte de habilidades. +settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidades +settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mensagem revertida diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index 5854b34dae..06b77491cf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -1,4 +1,6 @@ common.delete=Удалить +common.open=Открыть +common.save=Сохранить session.action.cancel=Отмена session.connection.connecting=Загрузка… session.connection.error.app=Ошибка подключения @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это. +settings.agentBehavior.skills.displayName=Навыки +settings.agentBehavior.skills.search=Фильтр навыков +settings.agentBehavior.skills.empty=Навыки не найдены. +settings.agentBehavior.skills.content.empty=Нет доступного содержимого навыка. +settings.agentBehavior.skills.load.timeout=Время загрузки навыков истекло. Существующие навыки сохранены; удалите медленные или недоступные URL и обновите. +settings.agentBehavior.skills.reload.deferred=Источник навыков сохранён. Перезагрузите Core после завершения активных сессий, чтобы применить новые навыки. +settings.agentBehavior.skills.reload.blocked=Настройки навыков сохранены, но есть активные сессии. Перезагрузите Core после завершения этих сессий, чтобы применить новые навыки. +settings.agentBehavior.skills.saved.notification=Настройки навыков сохранены +settings.agentBehavior.skills.delete.title=Удалить навык +settings.agentBehavior.skills.delete.message=Удалить навык {0}? Это удалит файл навыка, и действие нельзя будет отменить. +settings.agentBehavior.skills.delete.failed=Не удалось удалить навык. +settings.agentBehavior.skills.openInEditor=Открыть в редакторе +settings.agentBehavior.skills.openInEditor.pending=Файл навыка откроется после закрытия настроек. +settings.agentBehavior.skills.openInEditor.failed=Не удалось открыть файл навыка в редакторе. +settings.agentBehavior.skills.sources.empty=Источники навыков не настроены. +settings.agentBehavior.skills.sources.title=Дополнительные источники навыков +settings.agentBehavior.skills.sources.add=Добавить +settings.agentBehavior.skills.sources.addPath=Добавить путь +settings.agentBehavior.skills.sources.addPath.title=Добавить путь к навыкам +settings.agentBehavior.skills.sources.addPath.prompt=Выберите папку, содержащую навыки Kilo. +settings.agentBehavior.skills.sources.addUrl=Добавить URL +settings.agentBehavior.skills.sources.addUrl.title=Добавить URL источника навыков +settings.agentBehavior.skills.sources.addUrl.prompt=Введите URL источника навыков. +settings.agentBehavior.skills.sources.editPath.title=Изменить путь к навыкам +settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Отменено сообщений: {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index 1d79743dbf..b20b5127aa 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -1,4 +1,6 @@ common.delete=ลบ +common.open=เปิด +common.save=บันทึก session.action.cancel=ยกเลิก session.connection.connecting=กำลังโหลด… session.connection.error.app=การเชื่อมต่อล้มเหลว @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้ +settings.agentBehavior.skills.displayName=ทักษะ +settings.agentBehavior.skills.search=กรองทักษะ +settings.agentBehavior.skills.empty=ไม่พบทักษะ +settings.agentBehavior.skills.content.empty=ไม่มีเนื้อหาทักษะที่พร้อมใช้งาน +settings.agentBehavior.skills.load.timeout=การโหลดทักษะหมดเวลา ระบบเก็บทักษะเดิมไว้แล้ว โปรดลบ URL ที่ช้าหรือเข้าถึงไม่ได้แล้วรีเฟรช +settings.agentBehavior.skills.reload.deferred=บันทึกแหล่งที่มาทักษะแล้ว โหลด Core ใหม่หลังจากเซสชันที่ใช้งานอยู่สิ้นสุดเพื่อใช้ทักษะใหม่ +settings.agentBehavior.skills.reload.blocked=บันทึกการตั้งค่าทักษะแล้ว แต่ยังมีเซสชันที่ใช้งานอยู่ โหลด Core ใหม่หลังจากเซสชันเหล่านั้นสิ้นสุดเพื่อใช้ทักษะใหม่ +settings.agentBehavior.skills.saved.notification=บันทึกการตั้งค่าทักษะแล้ว +settings.agentBehavior.skills.delete.title=ลบทักษะ +settings.agentBehavior.skills.delete.message=ลบทักษะ {0} หรือไม่ การดำเนินการนี้จะลบไฟล์ทักษะและไม่สามารถย้อนกลับได้ +settings.agentBehavior.skills.delete.failed=ไม่สามารถลบทักษะได้ +settings.agentBehavior.skills.openInEditor=เปิดในตัวแก้ไข +settings.agentBehavior.skills.openInEditor.pending=ไฟล์ทักษะจะเปิดหลังจากคุณปิดการตั้งค่า +settings.agentBehavior.skills.openInEditor.failed=ไม่สามารถเปิดไฟล์ทักษะในตัวแก้ไขได้ +settings.agentBehavior.skills.sources.empty=ไม่ได้กำหนดค่าแหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.title=แหล่งที่มาทักษะเพิ่มเติม +settings.agentBehavior.skills.sources.add=เพิ่ม +settings.agentBehavior.skills.sources.addPath=เพิ่มเส้นทาง +settings.agentBehavior.skills.sources.addPath.title=เพิ่มเส้นทางทักษะ +settings.agentBehavior.skills.sources.addPath.prompt=เลือกโฟลเดอร์ที่มีทักษะ Kilo +settings.agentBehavior.skills.sources.addUrl=เพิ่ม URL +settings.agentBehavior.skills.sources.addUrl.title=เพิ่ม URL แหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.addUrl.prompt=ป้อน URL แหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้นทางทักษะ +settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index 8a397a6650..84b039046a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -1,4 +1,6 @@ common.delete=Sil +common.open=Aç +common.save=Kaydet session.action.cancel=İptal session.connection.connecting=Yükleniyor… session.connection.error.app=Bağlantı hatası @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin. +settings.agentBehavior.skills.displayName=Beceriler +settings.agentBehavior.skills.search=Becerileri filtrele +settings.agentBehavior.skills.empty=Beceri bulunamadı. +settings.agentBehavior.skills.content.empty=Kullanılabilir beceri içeriği yok. +settings.agentBehavior.skills.load.timeout=Beceriler yüklenirken zaman aşımına uğradı. Mevcut beceriler korundu; yavaş veya ulaşılamayan URL'leri kaldırıp yenileyin. +settings.agentBehavior.skills.reload.deferred=Beceri kaynağı kaydedildi. Yeni becerileri uygulamak için etkin oturumlar bittikten sonra Core'u yeniden yükleyin. +settings.agentBehavior.skills.reload.blocked=Beceri ayarları kaydedildi, ancak etkin oturumlar var. Yeni becerileri uygulamak için bu oturumlar bittikten sonra Core'u yeniden yükleyin. +settings.agentBehavior.skills.saved.notification=Beceri ayarları kaydedildi +settings.agentBehavior.skills.delete.title=Beceriyi Sil +settings.agentBehavior.skills.delete.message={0} becerisi silinsin mi? Bu işlem beceri dosyasını kaldırır ve geri alınamaz. +settings.agentBehavior.skills.delete.failed=Beceri silinemedi. +settings.agentBehavior.skills.openInEditor=Düzenleyicide Aç +settings.agentBehavior.skills.openInEditor.pending=Beceri dosyası Ayarlar kapatıldıktan sonra açılacak. +settings.agentBehavior.skills.openInEditor.failed=Beceri dosyası düzenleyicide açılamadı. +settings.agentBehavior.skills.sources.empty=Yapılandırılmış beceri kaynağı yok. +settings.agentBehavior.skills.sources.title=Ek Beceri Kaynakları +settings.agentBehavior.skills.sources.add=Ekle +settings.agentBehavior.skills.sources.addPath=Yol ekle +settings.agentBehavior.skills.sources.addPath.title=Beceri Yolu Ekle +settings.agentBehavior.skills.sources.addPath.prompt=Kilo becerilerini içeren bir klasör seçin. +settings.agentBehavior.skills.sources.addUrl=URL ekle +settings.agentBehavior.skills.sources.addUrl.title=Beceri Kaynağı URL'si Ekle +settings.agentBehavior.skills.sources.addUrl.prompt=Bir beceri kaynağı URL'si girin. +settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle +settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mesaj geri alındı diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 31ec8a2119..9e23de42ea 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -1,4 +1,6 @@ common.delete=Видалити +common.open=Відкрити +common.save=Зберегти session.action.cancel=Скасувати session.connection.connecting=Завантаження… session.connection.error.app=Помилка з'єднання @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це. +settings.agentBehavior.skills.displayName=Навички +settings.agentBehavior.skills.search=Фільтрувати навички +settings.agentBehavior.skills.empty=Навички не знайдено. +settings.agentBehavior.skills.content.empty=Немає доступного вмісту навички. +settings.agentBehavior.skills.load.timeout=Час завантаження навичок минув. Наявні навички збережено; видаліть повільні або недоступні URL-адреси й оновіть. +settings.agentBehavior.skills.reload.deferred=Джерело навичок збережено. Перезавантажте Core після завершення активних сеансів, щоб застосувати нові навички. +settings.agentBehavior.skills.reload.blocked=Налаштування навичок збережено, але є активні сеанси. Перезавантажте Core після завершення цих сеансів, щоб застосувати нові навички. +settings.agentBehavior.skills.saved.notification=Налаштування навичок збережено +settings.agentBehavior.skills.delete.title=Видалити навичку +settings.agentBehavior.skills.delete.message=Видалити навичку {0}? Це видалить файл навички, і дію не можна буде скасувати. +settings.agentBehavior.skills.delete.failed=Не вдалося видалити навичку. +settings.agentBehavior.skills.openInEditor=Відкрити в редакторі +settings.agentBehavior.skills.openInEditor.pending=Файл навички відкриється після закриття Налаштувань. +settings.agentBehavior.skills.openInEditor.failed=Не вдалося відкрити файл навички в редакторі. +settings.agentBehavior.skills.sources.empty=Джерела навичок не налаштовано. +settings.agentBehavior.skills.sources.title=Додаткові джерела навичок +settings.agentBehavior.skills.sources.add=Додати +settings.agentBehavior.skills.sources.addPath=Додати шлях +settings.agentBehavior.skills.sources.addPath.title=Додати шлях до навичок +settings.agentBehavior.skills.sources.addPath.prompt=Виберіть папку з навичками Kilo. +settings.agentBehavior.skills.sources.addUrl=Додати URL +settings.agentBehavior.skills.sources.addUrl.title=Додати URL джерела навичок +settings.agentBehavior.skills.sources.addUrl.prompt=Введіть URL джерела навичок. +settings.agentBehavior.skills.sources.editPath.title=Редагувати шлях до навичок +settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Відкочено повідомлень: {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index d729cc8309..961c7c64fa 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -1,4 +1,6 @@ common.delete=删除 +common.open=打开 +common.save=保存 session.action.cancel=取消 session.connection.connecting=加载中… session.connection.error.app=连接失败 @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。 +settings.agentBehavior.skills.displayName=技能 +settings.agentBehavior.skills.search=筛选技能 +settings.agentBehavior.skills.empty=未找到技能。 +settings.agentBehavior.skills.content.empty=没有可用的技能内容。 +settings.agentBehavior.skills.load.timeout=技能加载超时。已保留现有技能;请移除缓慢或无法访问的 URL 后刷新。 +settings.agentBehavior.skills.reload.deferred=技能源已保存。请在活动会话结束后重新加载 Core,以应用新技能。 +settings.agentBehavior.skills.reload.blocked=技能设置已保存,但仍有活动会话。请在这些会话结束后重新加载 Core,以应用新技能。 +settings.agentBehavior.skills.saved.notification=技能设置已保存 +settings.agentBehavior.skills.delete.title=删除技能 +settings.agentBehavior.skills.delete.message=要删除技能 {0} 吗?这会移除技能文件,且无法撤销。 +settings.agentBehavior.skills.delete.failed=无法删除该技能。 +settings.agentBehavior.skills.openInEditor=在编辑器中打开 +settings.agentBehavior.skills.openInEditor.pending=关闭设置后将打开技能文件。 +settings.agentBehavior.skills.openInEditor.failed=无法在编辑器中打开技能文件。 +settings.agentBehavior.skills.sources.empty=未配置技能源。 +settings.agentBehavior.skills.sources.title=其他技能源 +settings.agentBehavior.skills.sources.add=添加 +settings.agentBehavior.skills.sources.addPath=添加路径 +settings.agentBehavior.skills.sources.addPath.title=添加技能路径 +settings.agentBehavior.skills.sources.addPath.prompt=选择一个包含 Kilo 技能的文件夹。 +settings.agentBehavior.skills.sources.addUrl=添加 URL +settings.agentBehavior.skills.sources.addUrl.title=添加技能源 URL +settings.agentBehavior.skills.sources.addUrl.prompt=输入技能源 URL。 +settings.agentBehavior.skills.sources.editPath.title=编辑技能路径 +settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=已回滚 {0} 条消息 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index 2e0adac9a6..7b3461b72f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -1,4 +1,6 @@ common.delete=刪除 +common.open=開啟 +common.save=儲存 session.action.cancel=取消 session.connection.connecting=載入中… session.connection.error.app=連線失敗 @@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。 +settings.agentBehavior.skills.displayName=技能 +settings.agentBehavior.skills.search=篩選技能 +settings.agentBehavior.skills.empty=找不到技能。 +settings.agentBehavior.skills.content.empty=沒有可用的技能內容。 +settings.agentBehavior.skills.load.timeout=技能載入逾時。已保留現有技能;請移除緩慢或無法連線的 URL 後重新整理。 +settings.agentBehavior.skills.reload.deferred=技能來源已儲存。請在作用中工作階段結束後重新載入 Core,以套用新技能。 +settings.agentBehavior.skills.reload.blocked=技能設定已儲存,但仍有作用中工作階段。請在這些工作階段結束後重新載入 Core,以套用新技能。 +settings.agentBehavior.skills.saved.notification=技能設定已儲存 +settings.agentBehavior.skills.delete.title=刪除技能 +settings.agentBehavior.skills.delete.message=要刪除技能 {0} 嗎?這會移除技能檔案,且無法復原。 +settings.agentBehavior.skills.delete.failed=無法刪除該技能。 +settings.agentBehavior.skills.openInEditor=在編輯器中開啟 +settings.agentBehavior.skills.openInEditor.pending=關閉設定後將開啟技能檔案。 +settings.agentBehavior.skills.openInEditor.failed=無法在編輯器中開啟技能檔案。 +settings.agentBehavior.skills.sources.empty=未設定技能來源。 +settings.agentBehavior.skills.sources.title=其他技能來源 +settings.agentBehavior.skills.sources.add=新增 +settings.agentBehavior.skills.sources.addPath=新增路徑 +settings.agentBehavior.skills.sources.addPath.title=新增技能路徑 +settings.agentBehavior.skills.sources.addPath.prompt=選擇包含 Kilo 技能的資料夾。 +settings.agentBehavior.skills.sources.addUrl=新增 URL +settings.agentBehavior.skills.sources.addUrl.title=新增技能來源 URL +settings.agentBehavior.skills.sources.addUrl.prompt=輸入技能來源 URL。 +settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑 +settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=已回復 {0} 則訊息 From 1ffdd568b53998aeadf3d24efb005ef4c9a7b67c Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 20 Jul 2026 15:03:37 -0400 Subject: [PATCH 9/9] test(jetbrains): stabilize connection startup wait --- .../backend/app/KiloConnectionServiceTest.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt index 853b77f25b..254417db09 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import okhttp3.Request import okhttp3.sse.EventSource import okhttp3.sse.EventSourceListener @@ -36,6 +37,10 @@ import kotlin.test.assertTrue class KiloConnectionServiceTest { + private companion object { + const val WAIT_MS = 15_000L + } + private val mock = MockCliServer() private val fake = FakeCliServer(mock) private val log = TestLog() @@ -101,20 +106,23 @@ class KiloConnectionServiceTest { val svc = KiloConnectionService(scope, server, {}, log) val job = scope.launch { svc.connect() } - val downloading = withTimeout(5_000) { + val downloading = withTimeout(WAIT_MS) { svc.state.first { it is ConnectionState.Downloading } } assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading) resolved.complete(Unit) - withTimeout(5_000) { + withTimeout(WAIT_MS) { svc.state.first { it == ConnectionState.Connecting } } ready.complete(Unit) - withTimeout(5_000) { + val connected = withTimeoutOrNull(WAIT_MS) { svc.state.first { it is ConnectionState.Connected } } + if (connected == null) { + error("Timed out waiting for Connected after CLI ready; state=${svc.state.value}; logs=${log.messages.joinToString("\n")}") + } job.join() }