From ff149d4d726d0c6d3da9c54ff8047dc8c581a02b Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Fri, 31 Jul 2026 11:51:10 +0200 Subject: [PATCH 1/4] feat(jetbrains): show skill-shell command batch in permission card --- .../kilocode/backend/cli/KiloCliDataParser.kt | 8 ++ .../backend/cli/KiloCliDataParserTest.kt | 39 ++++++ .../session/controller/SessionController.kt | 1 + .../client/session/model/Permission.kt | 2 + .../views/permission/PermissionView.kt | 46 +++++++- .../resources/messages/KiloBundle.properties | 1 + .../views/permission/PermissionViewTest.kt | 111 ++++++++++++++++++ .../kotlin/ai/kilocode/rpc/dto/ChatDto.kt | 2 + 8 files changed, 207 insertions(+), 3 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 d4e376bb81..e5ff2bae92 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 @@ -1237,6 +1237,7 @@ object KiloCliDataParser { ruleDecisions = rawRules.ifEmpty { always.map { PermissionRuleDecisionDto(it) } }, filePath = path, fileDiffs = diffs, + skillCommands = metaObj.skillCommands(), ) } @@ -1653,6 +1654,13 @@ private fun JsonObject?.path(): String? { return str("filepath") ?: str("filePath") ?: str("file") ?: str("path") } +// metadata.commands is the verbatim skill-shell command list; the flat meta map loses the +// array, so read it as a list for the prompt to display. +private fun JsonObject?.skillCommands(): List { + if (this == null) return emptyList() + return this["commands"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() +} + private fun JsonObject?.ruleDecisions(): List { if (this == null) return emptyList() val raw = this["rules"] ?: return emptyList() diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 2a32a5060b..1390cbf8ed 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -2299,6 +2299,21 @@ class KiloCliDataParserTest { assertTrue(result.contains(""""message":"approved"""")) } + @Test + fun `buildPermissionReplyJson - interactive reply serializes interactive true`() { + // Wire contract the CLI server checks (permission/index.ts requires interactive + // === true to accept a non-reject reply to a skill-shell batch); a serialization + // regression here would silently break the entire approval flow. + val result = KiloCliDataParser.buildPermissionReplyJson(PermissionReplyDto(reply = "once", interactive = true)) + assertEquals("""{"reply":"once","interactive":true}""", result) + } + + @Test + fun `buildPermissionReplyJson - non-interactive reply omits the interactive field`() { + val result = KiloCliDataParser.buildPermissionReplyJson(PermissionReplyDto(reply = "once", interactive = false)) + assertFalse(result.contains("interactive"), "interactive must be omitted for a machine (non-interactive) reply, got: $result") + } + // ---- buildPermissionAlwaysRulesJson ---- @Test @@ -2435,6 +2450,30 @@ class KiloCliDataParserTest { assertEquals("git status --short", asked.request.metadata["command"]) } + @Test + fun `parsePermissionRequest - skill shell commands and skill name extracted`() { + val data = globalEvent(""" + "type": "permission.asked", + "properties": { + "id": "perm_skill", + "sessionID": "ses_1", + "permission": "bash", + "patterns": ["git status"], + "always": [], + "metadata": {"skillShell": true, "skill": "git-status", "commands": ["git status", "printf hi"]} + } + """) + + val result = KiloCliDataParser.parseChatEvent("permission.asked", data) + assertNotNull(result) + val asked = result as? ChatEventDto.PermissionAsked ?: error("Expected PermissionAsked") + // verbatim commands are parsed as a list for the prompt to display + assertEquals(listOf("git status", "printf hi"), asked.request.skillCommands) + // skillShell + skill name survive the flat metadata map for card attribution + assertEquals("true", asked.request.metadata["skillShell"]) + assertEquals("git-status", asked.request.metadata["skill"]) + } + @Test fun `parsePermissionRequest - parses rule decisions`() { val data = globalEvent(""" diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index d032d281cc..a79cd51e5d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -2511,6 +2511,7 @@ private fun toPermission(dto: PermissionRequestDto): Permission { fileDiff = diffs.firstOrNull(), fileDiffs = diffs, raw = dto.metadata, + skillCommands = dto.skillCommands, ), message = dto.message ?: dto.metadata["message"], tool = ref, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt index 09f4ea7d97..83fc19a30b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt @@ -27,6 +27,8 @@ data class PermissionMeta( val fileDiff: PermissionFileDiff? = null, val fileDiffs: List = emptyList(), val raw: Map = emptyMap(), + // Verbatim skill-shell commands to display when raw["skillShell"] == "true". + val skillCommands: List = emptyList(), ) data class PermissionRuleCandidate( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt index 0ae649ef54..e5dc870e9d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt @@ -116,15 +116,30 @@ class PermissionView( val prev = requestId requestId = permission.id - card.setHeader(KiloBundle.message("session.permission.title")) + val skillShell = permission.meta.raw["skillShell"] == "true" + val skill = permission.meta.raw["skill"] + card.setHeader( + if (skillShell && !skill.isNullOrBlank()) + KiloBundle.message("session.permission.skillShell.title", skill) + else KiloBundle.message("session.permission.title"), + ) syncDescription(description(permission)) val tool = permission.name - val target = if (tool == "bash") permission.meta.command else resolveTarget(permission) + // A skill-shell bash batch shows the verbatim command list (control-char-escaped so the + // displayed command can't repaint the line). Its external_directory sibling still shows + // directories via resolveTarget; only the header carries the skill attribution. + val target = when { + skillShell && tool == "bash" -> permission.meta.skillCommands.joinToString("\n") { escapeControl(it) } + tool == "bash" -> permission.meta.command + else -> resolveTarget(permission) + } syncCode(tool, target) syncDiffs(permission.meta.fileDiffs) responding = permission.state == PermissionRequestState.RESPONDING || permission.state == PermissionRequestState.RESOLVED - rules.update(permission.meta.ruleDecisions, reset = prev != permission.id) + // Skill-shell approvals are never persisted, so no auto-approve rule toggles even if a + // future backend change starts sending candidates for this batch. + rules.update(if (skillShell) emptyList() else permission.meta.ruleDecisions, reset = prev != permission.id) syncState(permission) syncPrimaryText() @@ -265,6 +280,31 @@ class PermissionView( view.component.border = JBUI.Borders.empty() } + // Escape control chars (CR/LF/ESC/etc.) and bidi/format characters so a skill command can't + // repaint the prompt or use Trojan-Source reordering to make the displayed text differ from + // what executes; newlines become a visible marker. Mirrors displayCommand in the CLI + // (packages/opencode/src/kilocode/skills/display.ts); keep the ranges in sync. + private fun escapeControl(command: String): String = buildString { + for (ch in command) { + val code = ch.code + when { + ch == '\n' -> append("\\n") + ch == '\r' -> append("\\r") + ch == '\t' -> append("\\t") + isEscapedControlOrFormat(code) -> append(if (code <= 0xff) "\\x%02x".format(code) else "\\u%04x".format(code)) + else -> append(ch) + } + } + } + + private fun isEscapedControlOrFormat(code: Int): Boolean = + code < 0x20 || + code in 0x7f..0x9f || + code in 0x200e..0x200f || + code in 0x2028..0x2029 || + code in 0x202a..0x202e || + code in 0x2066..0x2069 + private fun description(permission: Permission): String = if (permission.name == "bash") { permission.meta.raw["description"] ?: toolLabel(permission.name) } 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 6f26bb566d..aab7c511ac 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -52,6 +52,7 @@ session.error.revert.timeout=Operation timed out. Waiting for it to finish befor session.permission.title=Permission required session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Run shell commands from skill "{0}"? session.permission.meta=Tool: {0} • Patterns: {1} session.permission.run=Run session.permission.ask=Ask diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt index 83cec9799c..c577b60a7c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt @@ -164,6 +164,117 @@ class PermissionViewTest : BasePlatformTestCase() { assertFalse("Should not show state message for PENDING, got: $text", text.contains("Run this command?")) } + fun `test skill-shell bash permission shows skill-named header and verbatim commands`() { + view.show( + Permission( + id = "perm_skill", + sessionId = "ses", + name = "bash", + patterns = listOf("git status"), + always = emptyList(), + meta = PermissionMeta( + raw = mapOf("skillShell" to "true", "skill" to "git-status"), + skillCommands = listOf("git status", "printf hi"), + ), + ) + ) + + val text = allText(view) + assertTrue("Expected skill-named header, got: $text", text.contains("Run shell commands from skill \"git-status\"?")) + assertFalse("Should not show generic header, got: $text", text.contains("Permission required")) + val label = view.codeLabelsForTest().single() + assertTrue("Expected first verbatim command, got: ${label.text}", label.text.contains("git status")) + assertTrue("Expected second verbatim command, got: ${label.text}", label.text.contains("printf hi")) + } + + fun `test skill-shell permission without a skill name falls back to the generic header`() { + view.show( + Permission( + id = "perm_skill_noname", + sessionId = "ses", + name = "bash", + patterns = listOf("printf hi"), + always = emptyList(), + meta = PermissionMeta( + raw = mapOf("skillShell" to "true"), + skillCommands = listOf("printf hi"), + ), + ) + ) + + val text = allText(view) + assertTrue("Expected fallback header, got: $text", text.contains("Permission required")) + } + + fun `test skill-shell external_directory sibling shows the directory target, not the command list`() { + view.show( + Permission( + id = "perm_skill_dir", + sessionId = "ses", + name = "external_directory", + patterns = listOf("/tmp/*"), + always = emptyList(), + meta = PermissionMeta( + raw = mapOf("skillShell" to "true", "skill" to "git-status"), + skillCommands = listOf("cd /tmp && pwd"), + ), + ) + ) + + val text = allText(view) + // The header still names the skill — both asks in the batch carry the same metadata. + assertTrue("Expected skill-named header, got: $text", text.contains("Run shell commands from skill \"git-status\"?")) + val label = view.codeLabelsForTest().single() + assertEquals("/tmp/*", label.text) + assertFalse("Should not show the verbatim command list, got: ${label.text}", label.text.contains("cd /tmp")) + } + + fun `test skill-shell commands are escaped for control and bidi characters`() { + view.show( + Permission( + id = "perm_skill_escape", + sessionId = "ses", + name = "bash", + patterns = listOf("printf hi"), + always = emptyList(), + meta = PermissionMeta( + raw = mapOf("skillShell" to "true", "skill" to "git-status"), + // \u202e (RLO) would otherwise reverse the trailing text in the prompt. + skillCommands = listOf("rm \u202etxt.exe", "a\nb"), + ), + ) + ) + + val label = view.codeLabelsForTest().single() + assertTrue("Expected escaped RLO, got: ${label.text}", label.text.contains("rm \\u202etxt.exe")) + assertTrue("Expected escaped newline, got: ${label.text}", label.text.contains("a\\nb")) + assertFalse("Raw control char must not reach the label", label.text.contains("\u202e")) + } + + fun `test skill-shell permission never shows auto-approve rule toggles`() { + view.show( + Permission( + id = "perm_skill_norules", + sessionId = "ses", + name = "bash", + patterns = listOf("git status"), + always = listOf("git status"), + meta = PermissionMeta( + raw = mapOf("skillShell" to "true", "skill" to "git-status"), + skillCommands = listOf("git status"), + // Simulates a future backend sending rule candidates alongside skillShell + // metadata; approvals must still never be persisted for a skill batch. + ruleDecisions = listOf(PermissionRuleCandidate("git status")), + ), + ) + ) + + val text = allText(view) + assertFalse("Should not contain rules title, got: $text", text.contains("Auto-approve Rules")) + assertFalse(view.rulesForTest().isVisible) + assertEquals("Allow once", view.runButtonForTest().text) + } + fun `test non-bash patterns show action and path in editor`() { view.show( Permission( diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index f4e222bbb2..2246c30bb3 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -309,6 +309,8 @@ data class PermissionRequestDto( val ruleDecisions: List = emptyList(), val filePath: String? = null, val fileDiffs: List = emptyList(), + // Verbatim skill-shell commands (metadata.commands) the prompt must display; empty for non-skill requests. + val skillCommands: List = emptyList(), ) @Serializable From 86e55070a72175b65f5af4d285876aad23cfd70b Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Fri, 31 Jul 2026 12:06:41 +0200 Subject: [PATCH 2/4] i18n(jetbrains): translate skill-shell permission card title --- .../src/main/resources/messages/KiloBundle_ar.properties | 1 + .../src/main/resources/messages/KiloBundle_bs.properties | 1 + .../src/main/resources/messages/KiloBundle_da.properties | 1 + .../src/main/resources/messages/KiloBundle_de.properties | 1 + .../src/main/resources/messages/KiloBundle_es.properties | 1 + .../src/main/resources/messages/KiloBundle_fr.properties | 1 + .../src/main/resources/messages/KiloBundle_ja.properties | 1 + .../src/main/resources/messages/KiloBundle_ko.properties | 1 + .../src/main/resources/messages/KiloBundle_nl.properties | 1 + .../src/main/resources/messages/KiloBundle_no.properties | 1 + .../src/main/resources/messages/KiloBundle_pl.properties | 1 + .../src/main/resources/messages/KiloBundle_pt_BR.properties | 1 + .../src/main/resources/messages/KiloBundle_ru.properties | 1 + .../src/main/resources/messages/KiloBundle_th.properties | 1 + .../src/main/resources/messages/KiloBundle_tr.properties | 1 + .../src/main/resources/messages/KiloBundle_uk.properties | 1 + .../src/main/resources/messages/KiloBundle_zh_CN.properties | 1 + .../src/main/resources/messages/KiloBundle_zh_TW.properties | 1 + 18 files changed, 18 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 999dcb7c54..15944a645d 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=هل تريد تشغيل أوامر الصدفة من المهارة «{0}»؟ session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 fb61208a23..9627408969 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Pokrenuti shell komande iz vještine „{0}"? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 a785788ba0..bcdfac3884 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Kør shell-kommandoer fra færdigheden „{0}"? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 837e4d8dd7..26f5656dae 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Shell-Befehle aus dem Skill „{0}" ausführen? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 25279f3492..0d19e12cfd 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=¿Ejecutar comandos de shell de la habilidad «{0}»? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 3c279709d2..683bf55d43 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Exécuter les commandes shell de la compétence « {0} » ? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 9d853d2a79..e9e1c52f3c 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=スキル「{0}」のシェルコマンドを実行しますか? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 9b1797052c..c2e935a4ed 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=스킬 "{0}"의 셸 명령을 실행할까요? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 85fc3f6b31..e43ba741bd 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Shell-opdrachten uit vaardigheid "{0}" uitvoeren? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 c160fdf400..d391bcc32a 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Kjøre skallkommandoer fra ferdigheten «{0}»? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 db9776e10b..2e8b08d9ca 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Uruchomić polecenia powłoki z umiejętności „{0}"? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 35dda74bfe..2bf9c4c4cc 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Executar comandos de shell da skill "{0}"? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 3d53c6262d..aef654244b 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Выполнить команды оболочки из навыка «{0}»? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 3b400b23d0..8959598aa7 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=เรียกใช้คำสั่งเชลล์จากสกิล "{0}" หรือไม่? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 8b7a2de3cd..b954188a5c 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 @@ -177,6 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title="{0}" becerisindeki kabuk komutları çalıştırılsın mı? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 896ee37e7a..77ea64c775 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=Виконати команди оболонки з навички «{0}»? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 864bce7a28..909073cae3 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=要执行技能「{0}」的 shell 命令吗? session.permission.run=Run session.permission.command=Command session.permission.patterns={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 48c5418ae7..bc74d81c93 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 @@ -172,6 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) +session.permission.skillShell.title=要執行技能「{0}」的 shell 指令嗎? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: From 5577903a60f4b6162296282d99437cad0ed9238e Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Fri, 31 Jul 2026 12:14:34 +0200 Subject: [PATCH 3/4] fix(jetbrains): escape the skill name in the skill-shell permission header --- .../views/permission/PermissionView.kt | 4 +++- .../views/permission/PermissionViewTest.kt | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt index e5dc870e9d..91f299579f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt @@ -120,7 +120,9 @@ class PermissionView( val skill = permission.meta.raw["skill"] card.setHeader( if (skillShell && !skill.isNullOrBlank()) - KiloBundle.message("session.permission.skillShell.title", skill) + // skill is the untrusted SKILL.md frontmatter name; escape it the same way as + // the command list so it can't reorder/repaint the header. + KiloBundle.message("session.permission.skillShell.title", escapeControl(skill)) else KiloBundle.message("session.permission.title"), ) syncDescription(description(permission)) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt index c577b60a7c..490c226ad0 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt @@ -251,6 +251,28 @@ class PermissionViewTest : BasePlatformTestCase() { assertFalse("Raw control char must not reach the label", label.text.contains("\u202e")) } + fun `test skill-shell header escapes control and bidi characters in the skill name`() { + view.show( + Permission( + id = "perm_skill_name_escape", + sessionId = "ses", + name = "bash", + patterns = listOf("printf hi"), + always = emptyList(), + meta = PermissionMeta( + // The skill name is untrusted SKILL.md frontmatter; a bidi override here + // must not be able to reorder the header's attribution text. + raw = mapOf("skillShell" to "true", "skill" to "git-status\u202e"), + skillCommands = listOf("printf hi"), + ), + ) + ) + + val text = allText(view) + assertTrue("Expected escaped RLO in header, got: $text", text.contains("git-status\\u202e")) + assertFalse("Raw control char must not reach the header, got: $text", text.contains("git-status\u202e")) + } + fun `test skill-shell permission never shows auto-approve rule toggles`() { view.show( Permission( From e804131f20dd9e272c57b0b05cfb3c9c855e806a Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Fri, 31 Jul 2026 12:18:47 +0200 Subject: [PATCH 4/4] i18n(jetbrains): fix mismatched closing quotes in skill-shell title --- .../src/main/resources/messages/KiloBundle_bs.properties | 2 +- .../src/main/resources/messages/KiloBundle_da.properties | 2 +- .../src/main/resources/messages/KiloBundle_de.properties | 2 +- .../src/main/resources/messages/KiloBundle_pl.properties | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 9627408969..df5882b500 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 @@ -172,7 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) -session.permission.skillShell.title=Pokrenuti shell komande iz vještine „{0}"? +session.permission.skillShell.title=Pokrenuti shell komande iz vještine „{0}“? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 bcdfac3884..65d9d0909c 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 @@ -172,7 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) -session.permission.skillShell.title=Kør shell-kommandoer fra færdigheden „{0}"? +session.permission.skillShell.title=Kør shell-kommandoer fra færdigheden „{0}“? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 26f5656dae..9b0ca26632 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 @@ -172,7 +172,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) -session.permission.skillShell.title=Shell-Befehle aus dem Skill „{0}" ausführen? +session.permission.skillShell.title=Shell-Befehle aus dem Skill „{0}“ ausführen? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: 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 2e8b08d9ca..ef4c5efe13 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 @@ -177,7 +177,7 @@ session.account.switcher=Switch account session.scroll.question=Scroll to question session.permission.title.subagent=Permission required (subagent) -session.permission.skillShell.title=Uruchomić polecenia powłoki z umiejętności „{0}"? +session.permission.skillShell.title=Uruchomić polecenia powłoki z umiejętności „{0}”? session.permission.run=Run session.permission.command=Command session.permission.patterns={0}: