Skip to content
2 changes: 1 addition & 1 deletion packages/kilo-vscode/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default [
{
files: ["src/KiloProvider.ts"],
rules: {
"max-lines": ["error", 3200],
"max-lines": ["error", 3300],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably refactor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, @imanolmzd-svg indicated he was looking into it, but I don't think this specific pr is a big offender

},
},
eslintConfigPrettier,
Expand Down
25 changes: 16 additions & 9 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,26 @@ import {
saveCustomProvider as saveCustomProviderAction,
} from "./provider-actions"
import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models"
import type { Agent } from "@kilocode/sdk/v2/client"

type KiloProviderOptions = {
projectDirectory?: string | null
slimEditMetadata?: boolean
}

// Helper to map agent data to the subset of fields sent to the webview
const mapAgent = (a: Agent) => ({
name: a.name,
displayName: a.displayName,
description: a.description,
mode: a.mode,
native: a.native,
hidden: a.hidden,
color: a.color,
deprecated: a.deprecated,
permission: a.permission,
})

export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider {
public static readonly viewType = "kilo-code.SidebarProvider"

Expand Down Expand Up @@ -1625,15 +1639,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ const AgentBehaviourTab: Component = () => {
})

const agentNames = createMemo(() => {
const names = session.agents().map((a) => a.name)
// Exclude server-side hidden internal modes (compaction, title, summary)
// from the list. Config-only agents are still added below.
const names = session
.allAgents()
.filter((a) => !a.hidden)
.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) {
Expand All @@ -93,10 +98,15 @@ const AgentBehaviourTab: Component = () => {
return names.sort()
})

const defaultAgentOptions = createMemo<SelectOption[]>(() => [
{ value: "", label: language.t("common.default") },
...agentNames().map((name) => ({ value: name, label: name })),
])
// Default-agent picker must only show visible primary agents (not subagents
// or hidden modes) since the CLI rejects those as default_agent values.
const defaultAgentOptions = createMemo<SelectOption[]>(() => {
const visible = session.agents().map((a) => a.name)
return [
{ value: "", label: language.t("common.default") },
...visible.map((name) => ({ value: name, label: name })),
]
})

const instructions = () => config().instructions ?? []

Expand Down Expand Up @@ -185,7 +195,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(() => (
Expand Down Expand Up @@ -354,7 +364,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
Expand Down Expand Up @@ -396,6 +406,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={{
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"

Expand All @@ -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] ?? {})

Expand Down Expand Up @@ -230,6 +231,18 @@ const ModeEditView: Component<Props> = (props) => {
</SettingsRow>
</Card>

{/* Calculated permissions (read-only, collapsible) */}
<Show when={agent()?.permission} keyed>
{(rules) => (
<PermissionRuleset
agent={props.name}
rules={rules}
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")}
Expand All @@ -239,4 +252,201 @@ 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)" },
unknown: { bg: "var(--vscode-descriptionForeground, #8b949e)", fg: "var(--vscode-editor-background, #1e1e1e)" },
}

interface RulesetProps {
agent: string
rules: PermissionRuleItem[]
expanded: boolean
onToggle: () => void
}

const PermissionRuleset: Component<RulesetProps> = (props) => {
const language = useLanguage()
const [copied, setCopied] = createSignal(false)

// Compute effective action per unique tool by finding the last rule with pattern "*"
// NOTE: This assumes the CLI uses "*" as the wildcard pattern for catch-all rules.
// If the CLI convention changes (e.g. to "**" or another pattern), this will need updating.
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Wildcard permission rules render a misleading summary

The CLI already emits catch-all rules like { permission: "*", pattern: "*", action: "allow" }. In that case this memo records a single * entry, so the new "Effective summary" shows *: allow instead of the effective action for each tool. That makes the summary disagree with the runtime ruleset for agents whose defaults come from wildcard permissions.

}
}
return [...tools.entries()].sort((a, b) => a[0].localeCompare(b[0]))
})

const copy = (e: MouseEvent) => {
e.stopPropagation()
const data = { agent: props.agent, rules: props.rules }
navigator.clipboard.writeText(JSON.stringify(data, null, 2))
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}

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 style={{ "margin-left": "auto" }}>
<IconButton
size="small"
variant="ghost"
icon={copied() ? "check" : "copy"}
title={language.t("settings.agentBehaviour.permissions.copy")}
onClick={copy}
/>
</div>
</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.unknown
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.unknown
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
Loading
Loading