feat(vscode): add calculated permissions display to agent details

Show the resolved permission ruleset for each agent in the Agent
Behaviour settings tab. The section is collapsed by default and
expands to show an effective-action summary plus the full ordered
rule table (last match wins).

Changes:
- Forward all agents (including subagents) with their CLI-calculated
  permission rulesets to the webview via a new allAgents field
- Add collapsible PermissionRuleset component in ModeEditView
- Show subagents in the Agent Behaviour agent list with a badge
- Add PermissionRuleItem type mirroring the backend PermissionNext.Rule

Ref: https://github.com/Kilo-Org/kilocode/issues/8331
This commit is contained in:
kiloconnect[bot]
2026-04-08 13:36:24 +02:00
committed by Mark IJbema
parent c7bebd225e
commit bd0295b5e2
6 changed files with 235 additions and 15 deletions
+13 -9
View File
@@ -1623,17 +1623,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const { visible, defaultAgent } = filterVisibleAgents(agents)
const mapAgent = (a: (typeof agents)[number]) => ({
name: a.name,
displayName: a.displayName,
description: a.description,
mode: a.mode,
native: a.native,
color: a.color,
deprecated: a.deprecated,
permission: a.permission,
})
const message = {
type: "agentsLoaded",
agents: visible.map((a) => ({
name: a.name,
displayName: a.displayName,
description: a.description,
mode: a.mode,
native: a.native,
color: a.color,
deprecated: a.deprecated,
})),
agents: visible.map(mapAgent),
allAgents: agents.map(mapAgent),
defaultAgent,
}
this.cachedAgentsMessage = message
@@ -82,7 +82,7 @@ const AgentBehaviourTab: Component = () => {
})
const agentNames = createMemo(() => {
const names = session.agents().map((a) => a.name)
const names = session.allAgents().map((a) => a.name)
// Also include any agents from config that might not be in the agent list
const agents = Object.keys(config().agent ?? {})
for (const name of agents) {
@@ -185,7 +185,7 @@ const AgentBehaviourTab: Component = () => {
))
}
const removableModes = createMemo(() => session.agents().filter((a) => !a.native))
const removableModes = createMemo(() => session.allAgents().filter((a) => !a.native))
const confirmRemoveMode = (agent: AgentInfo) => {
dialog.show(() => (
@@ -354,7 +354,7 @@ const AgentBehaviourTab: Component = () => {
<Card style={{ "margin-bottom": "12px" }}>
<For each={agentNames()}>
{(name, index) => {
const agent = () => session.agents().find((a) => a.name === name)
const agent = () => session.allAgents().find((a) => a.name === name)
const isCustom = () => !agent()?.native
const agentCfg = () => config().agent?.[name] ?? {}
const disabled = () => agentCfg().disable ?? false
@@ -396,6 +396,19 @@ const AgentBehaviourTab: Component = () => {
custom
</span>
</Show>
<Show when={agent()?.mode === "subagent"}>
<span
style={{
"font-size": "10px",
padding: "1px 5px",
"border-radius": "3px",
background: "var(--bg-subtle-base, var(--vscode-badge-background))",
color: "var(--text-weak-base, var(--vscode-badge-foreground))",
}}
>
{language.t("settings.agentBehaviour.badge.subagent")}
</span>
</Show>
<Show when={hidden()}>
<span
style={{
@@ -1,4 +1,4 @@
import { Component, Show, createMemo } from "solid-js"
import { Component, Show, For, createMemo, createSignal } from "solid-js"
import { TextField } from "@kilocode/kilo-ui/text-field"
import { Switch } from "@kilocode/kilo-ui/switch"
import { Card } from "@kilocode/kilo-ui/card"
@@ -8,7 +8,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { useConfig } from "../../context/config"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
import type { AgentConfig, AgentInfo } from "../../types/messages"
import type { AgentConfig, AgentInfo, PermissionRuleItem } from "../../types/messages"
import SettingsRow from "./SettingsRow"
import { buildExport } from "./mode-io"
@@ -26,8 +26,9 @@ const ModeEditView: Component<Props> = (props) => {
// agent() may be undefined for modes that only exist in the config draft (just
// created, not yet saved). This is fine — native defaults to false (correct for
// custom modes) and all fields read from cfg() which comes from config context.
const agent = () => session.agents().find((a) => a.name === props.name)
const agent = () => session.allAgents().find((a) => a.name === props.name)
const native = () => agent()?.native ?? false
const [expanded, setExpanded] = createSignal(false)
const cfg = createMemo<AgentConfig>(() => config().agent?.[props.name] ?? {})
@@ -230,6 +231,11 @@ const ModeEditView: Component<Props> = (props) => {
</SettingsRow>
</Card>
{/* Calculated permissions (read-only, collapsible) */}
<Show when={agent()?.permission}>
<PermissionRuleset rules={agent()!.permission!} expanded={expanded()} onToggle={() => setExpanded((v) => !v)} />
</Show>
<div style={{ display: "flex", "justify-content": "flex-end" }}>
<Button variant="ghost" onClick={props.onBack}>
{language.t("settings.agentBehaviour.editMode.back")}
@@ -239,4 +245,179 @@ const ModeEditView: Component<Props> = (props) => {
)
}
// ---------------------------------------------------------------------------
// Collapsible permissions ruleset display
// ---------------------------------------------------------------------------
const ACTION_COLORS: Record<string, { bg: string; fg: string }> = {
allow: { bg: "var(--vscode-terminal-ansiGreen, #3fb950)", fg: "var(--vscode-editor-background, #1e1e1e)" },
ask: { bg: "var(--vscode-editorWarning-foreground, #cca700)", fg: "var(--vscode-editor-background, #1e1e1e)" },
deny: { bg: "var(--vscode-errorForeground, #f85149)", fg: "var(--vscode-editor-background, #fff)" },
}
interface RulesetProps {
rules: PermissionRuleItem[]
expanded: boolean
onToggle: () => void
}
const PermissionRuleset: Component<RulesetProps> = (props) => {
const language = useLanguage()
// Compute effective action per unique tool by finding the last rule with pattern "*"
const summary = createMemo(() => {
const tools = new Map<string, PermissionRuleItem["action"]>()
for (const rule of props.rules) {
if (rule.pattern === "*") {
tools.set(rule.permission, rule.action)
}
}
return [...tools.entries()].sort((a, b) => a[0].localeCompare(b[0]))
})
return (
<Card style={{ "margin-bottom": "12px" }}>
<div
style={{ display: "flex", "align-items": "center", cursor: "pointer", "user-select": "none" }}
onClick={props.onToggle}
>
<IconButton
size="small"
variant="ghost"
icon={props.expanded ? "chevron-down" : "chevron-right"}
onClick={(e: MouseEvent) => {
e.stopPropagation()
props.onToggle()
}}
/>
<span data-slot="settings-row-label-title" style={{ "margin-left": "4px" }}>
{language.t("settings.agentBehaviour.permissions.title")}
</span>
<span
style={{
"margin-left": "8px",
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
{language.t("settings.agentBehaviour.permissions.count", { count: String(props.rules.length) })}
</span>
</div>
<Show when={props.expanded}>
{/* Summary: effective action per tool for wildcard pattern */}
<Show when={summary().length > 0}>
<div style={{ "margin-top": "8px", "margin-bottom": "8px" }}>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"margin-bottom": "4px",
}}
>
{language.t("settings.agentBehaviour.permissions.effective")}
</div>
<div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
<For each={summary()}>
{([tool, action]) => {
const colors = ACTION_COLORS[action] ?? ACTION_COLORS.ask
return (
<span
style={{
"font-size": "11px",
padding: "2px 6px",
"border-radius": "3px",
background: colors.bg,
color: colors.fg,
"font-family": "var(--vscode-editor-font-family, monospace)",
}}
>
{tool}: {action}
</span>
)
}}
</For>
</div>
</div>
</Show>
{/* Full ruleset table */}
<div
style={{
"margin-top": "8px",
"font-size": "11px",
"font-family": "var(--vscode-editor-font-family, monospace)",
"max-height": "300px",
"overflow-y": "auto",
border: "1px solid var(--border-weak-base, var(--vscode-panel-border))",
"border-radius": "4px",
}}
>
<table style={{ width: "100%", "border-collapse": "collapse" }}>
<thead>
<tr
style={{
background: "var(--bg-subtle-base, var(--vscode-editorWidget-background))",
position: "sticky",
top: "0",
}}
>
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
{language.t("settings.agentBehaviour.permissions.col.tool")}
</th>
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
{language.t("settings.agentBehaviour.permissions.col.pattern")}
</th>
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
{language.t("settings.agentBehaviour.permissions.col.action")}
</th>
</tr>
</thead>
<tbody>
<For each={props.rules}>
{(rule, idx) => {
const colors = ACTION_COLORS[rule.action] ?? ACTION_COLORS.ask
return (
<tr
style={{
"border-top":
idx() > 0 ? "1px solid var(--border-weak-base, var(--vscode-panel-border))" : "none",
}}
>
<td style={{ padding: "3px 8px" }}>{rule.permission}</td>
<td style={{ padding: "3px 8px", color: "var(--text-weak-base)" }}>{rule.pattern}</td>
<td style={{ padding: "3px 8px" }}>
<span
style={{
padding: "1px 4px",
"border-radius": "2px",
background: colors.bg,
color: colors.fg,
}}
>
{rule.action}
</span>
</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
<div
style={{
"margin-top": "6px",
"font-size": "10px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
{language.t("settings.agentBehaviour.permissions.hint")}
</div>
</Show>
</Card>
)
}
export default ModeEditView
@@ -134,6 +134,7 @@ interface SessionContextValue {
// Agent/mode selection (per-session)
agents: Accessor<AgentInfo[]>
allAgents: Accessor<AgentInfo[]>
removeMode: (name: string) => void
removeMcp: (name: string) => void
@@ -255,6 +256,7 @@ export const SessionProvider: ParentComponent = (props) => {
// Agents (modes) loaded from the CLI backend
const [agents, setAgents] = createSignal<AgentInfo[]>([])
const [allAgents, setAllAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
// Skills loaded from the CLI backend
@@ -468,6 +470,7 @@ export const SessionProvider: ParentComponent = (props) => {
return
}
setAgents(message.agents)
setAllAgents(message.allAgents ?? message.agents)
setDefaultAgent(message.defaultAgent)
const names = new Set(message.agents.map((a) => a.name))
@@ -1808,6 +1811,7 @@ export const SessionProvider: ParentComponent = (props) => {
costBreakdown,
contextUsage,
agents,
allAgents,
skills,
refreshSkills,
removeSkill,
@@ -1178,6 +1178,15 @@ export const dict = {
"settings.agentBehaviour.editMode.native":
"This is a built-in mode. Its base definition cannot be changed, but you can configure overrides below.",
"settings.agentBehaviour.editMode.promptOverride": "Custom prompt override for this built-in mode",
"settings.agentBehaviour.badge.subagent": "subagent",
"settings.agentBehaviour.permissions.title": "Calculated Permissions",
"settings.agentBehaviour.permissions.count": "{{count}} rules",
"settings.agentBehaviour.permissions.effective": "Effective (wildcard):",
"settings.agentBehaviour.permissions.col.tool": "Tool",
"settings.agentBehaviour.permissions.col.pattern": "Pattern",
"settings.agentBehaviour.permissions.col.action": "Action",
"settings.agentBehaviour.permissions.hint":
"Rules are evaluated in order — last matching rule wins. This is the resolved ruleset from the CLI backend.",
"settings.autoApprove.description":
"Define how tools are allowed to run. Most tools default to Allow. doom_loop and external_directory default to Ask.",
@@ -213,6 +213,13 @@ export interface SlashCommandInfo {
hints: string[]
}
// A single resolved permission rule from the CLI backend (matches PermissionNext.Rule)
export interface PermissionRuleItem {
permission: string
pattern: string
action: PermissionLevel
}
// Agent/mode info from CLI backend
export interface AgentInfo {
name: string
@@ -223,6 +230,7 @@ export interface AgentInfo {
hidden?: boolean
deprecated?: boolean
color?: string
permission?: PermissionRuleItem[]
}
// Server info
@@ -656,6 +664,7 @@ export interface ProvidersLoadedMessage {
export interface AgentsLoadedMessage {
type: "agentsLoaded"
agents: AgentInfo[]
allAgents: AgentInfo[]
defaultAgent: string
}