mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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:
committed by
Mark IJbema
parent
c7bebd225e
commit
bd0295b5e2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user