From c947e2a04dd0a4cd45eb843affc44c4c6ea586ba Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sat, 25 Apr 2026 01:02:25 +0530 Subject: [PATCH 1/6] feat/add enable/disable toggle for skills and rules model --- packages/kilo-vscode/webview-ui/src/types/messages.ts | 1 + packages/opencode/src/config/permission.ts | 1 + packages/opencode/src/config/skills.ts | 1 + packages/opencode/src/permission/evaluate.ts | 3 ++- packages/opencode/src/permission/index.ts | 1 + packages/opencode/src/skill/index.ts | 7 ++++++- 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 473528888a6..fd5b509804b 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -435,6 +435,7 @@ export interface CommandConfig { export interface SkillsConfig { paths?: string[] urls?: string[] + disabled?: string[] } export interface CompactionConfig { diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index b29bd42631d..0bb24856fda 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -42,6 +42,7 @@ export const Info = z z .object({ __originalKeys: z.string().array().optional(), + disabled: z.array(z.string()).optional().describe("Patterns of disabled rules"), read: Rule.optional(), edit: Rule.optional(), glob: Rule.optional(), diff --git a/packages/opencode/src/config/skills.ts b/packages/opencode/src/config/skills.ts index 38cbf99e7d7..9272fca7dad 100644 --- a/packages/opencode/src/config/skills.ts +++ b/packages/opencode/src/config/skills.ts @@ -6,6 +6,7 @@ export const Info = z.object({ .array(z.string()) .optional() .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), + disabled: z.array(z.string()).optional().describe("Names of disabled skills"), }) export type Info = z.infer diff --git a/packages/opencode/src/permission/evaluate.ts b/packages/opencode/src/permission/evaluate.ts index bcc4e58118d..27a1980a749 100644 --- a/packages/opencode/src/permission/evaluate.ts +++ b/packages/opencode/src/permission/evaluate.ts @@ -4,10 +4,11 @@ type Rule = { permission: string pattern: string action: "allow" | "deny" | "ask" + enabled?: boolean } export function evaluate(permission: string, pattern: string, ...rulesets: Rule[][]): Rule { - const rules = rulesets.flat() + const rules = rulesets.flat().filter((rule) => rule.enabled !== false) const match = rules.findLast( (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), ) diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 6758f8a1576..529b1004fc4 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -32,6 +32,7 @@ export class Rule extends Schema.Class("PermissionRule")({ permission: Schema.String, pattern: Schema.String, action: Action, + enabled: Schema.optional(Schema.Boolean).pipe(Schema.withDefault(true)), }) { static readonly zod = zod(this) } diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 01e3b35556e..dc7ac420903 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -34,6 +34,7 @@ export const Info = z.object({ description: z.string(), location: z.string(), content: z.string(), + enabled: z.boolean().default(true), }) export type Info = z.infer @@ -260,7 +261,11 @@ export const layer = Layer.effect( const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { const s = yield* InstanceState.get(state) - const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name)) + const cfg = yield* config.get() + const disabledSkills = new Set(cfg.skills?.disabled ?? []) + const list = Object.values(s.skills) + .filter((skill) => !disabledSkills.has(skill.name)) + .toSorted((a, b) => a.name.localeCompare(b.name)) if (!agent) return list return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny") }) From 1d0bf5d5a2af24449aab262730c90ecdb8c17c58 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sat, 25 Apr 2026 01:08:30 +0530 Subject: [PATCH 2/6] add UI for toggle and skills edit modal --- .../components/settings/AgentBehaviourTab.tsx | 183 ++++++++++++++++-- .../components/settings/SkillEditModal.tsx | 149 ++++++++++++++ 2 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/settings/SkillEditModal.tsx diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index a583c71f0fc..bc18969e3f2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -17,6 +17,7 @@ import ModeEditView from "./ModeEditView" import ModeCreateView from "./ModeCreateView" import McpEditView from "./McpEditView" import WorkflowsTab from "./agent-behaviour/WorkflowsTab" +import { SkillEditModal } from "./SkillEditModal" import { parseImport, MAX_IMPORT_SIZE } from "./mode-io" import type { ImportError } from "./mode-io" @@ -74,6 +75,9 @@ const AgentBehaviourTab: Component = () => { // MCP view state const [editingMcp, setEditingMcp] = createSignal("") + // Skill edit state + const [editingSkill, setEditingSkill] = createSignal(null) + // Fetch skills whenever the skills subtab becomes active createEffect(() => { if (activeSubtab() === "skills") { @@ -170,6 +174,36 @@ const AgentBehaviourTab: Component = () => { updateConfig({ skills: { ...config().skills, urls: current } }) } + const disabledSkills = () => config().skills?.disabled ?? [] + + const toggleSkillEnabled = (skillName: string) => { + const disabled = [...disabledSkills()] + const index = disabled.indexOf(skillName) + if (index >= 0) { + disabled.splice(index, 1) + } else { + disabled.push(skillName) + } + updateConfig({ skills: { ...config().skills, disabled } }) + } + + const isSkillDisabled = (skillName: string) => disabledSkills().includes(skillName) + + const disabledRulePatterns = () => config().permission?.disabled ?? [] + + const toggleRuleEnabled = (pattern: string) => { + const disabled = [...disabledRulePatterns()] + const index = disabled.indexOf(pattern) + if (index >= 0) { + disabled.splice(index, 1) + } else { + disabled.push(pattern) + } + updateConfig({ permission: { ...config().permission, disabled } }) + } + + const isRuleDisabled = (pattern: string) => disabledRulePatterns().includes(pattern) + const confirmRemoveSkill = (skill: SkillInfo) => { dialog.show(() => ( @@ -821,26 +855,45 @@ const AgentBehaviourTab: Component = () => { "justify-content": "space-between", padding: "8px 0", "border-bottom": index() < session.skills().length - 1 ? "1px solid var(--border-weak-base)" : "none", + opacity: isSkillDisabled(skill.name) ? "0.6" : "1", + "text-decoration": isSkillDisabled(skill.name) ? "line-through" : "none", }} > -
-
- {skill.name} -
-
-
{skill.description}
- {skill.location !== "builtin" &&
{skill.location}
} +
+ toggleSkillEnabled(skill.name)} + hideLabel + /> +
+
+ {skill.name} +
+
+
{skill.description}
+ {skill.location !== "builtin" &&
{skill.location}
} +
- {skill.location !== "builtin" && ( - confirmRemoveSkill(skill)} /> - )} +
+ {skill.location !== "builtin" && ( + setEditingSkill(skill)} + /> + )} + {skill.location !== "builtin" && ( + confirmRemoveSkill(skill)} /> + )} +
)} @@ -1043,6 +1096,91 @@ const AgentBehaviourTab: Component = () => { + {/* Rules Toggle Section */} +

+ {language.t("settings.agentBehaviour.rules.title") || "Permission Rules"} +

+ +
+
+ {language.t("settings.agentBehaviour.rules.toggleDescription") || + "Enable or disable permission rules without deleting them"} +
+
+ + {/* Display discovered rules */} + 0 && + Object.keys(config().permission).some((k) => k !== "disabled" && k !== "__originalKeys") + } + fallback={ +
+ {language.t("settings.agentBehaviour.rules.noRules") || "No permission rules configured"} +
+ } + > + k !== "disabled" && k !== "__originalKeys", + )} + > + {(permission, index) => { + const allKeys = Object.keys(config().permission ?? {}).filter( + (k) => k !== "disabled" && k !== "__originalKeys", + ) + return ( +
+
+
+ {permission} +
+
+
+ toggleRuleEnabled(permission)} + hideLabel + /> +
+
+ ) + }} +
+
+
+ {/* Claude Code compatibility */}

{language.t("settings.agentBehaviour.claudeCompat.heading")} @@ -1140,6 +1278,19 @@ const AgentBehaviourTab: Component = () => { {/* Subtab content */} {renderSubtabContent()} + + {/* Skill Edit Modal */} + + {(skill) => ( + setEditingSkill(null)} + onSave={(updated) => { + session.refreshSkills() + }} + /> + )} +

) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SkillEditModal.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SkillEditModal.tsx new file mode 100644 index 00000000000..6066ad146e6 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SkillEditModal.tsx @@ -0,0 +1,149 @@ +import { createSignal, onCleanup } from "solid-js" +import { Dialog } from "@kilocode/kilo-ui/dialog" +import { Button } from "@kilocode/kilo-ui/button" +import { TextField } from "@kilocode/kilo-ui/text-field" +import { useLanguage } from "../../context/language" +import { useVSCode } from "../../context/vscode" +import type { SkillInfo } from "../../types/messages" + +interface Props { + skill: SkillInfo + onClose: () => void + onSave: (updatedSkill: { name: string; content: string }) => void +} + +export const SkillEditModal = (props: Props) => { + const language = useLanguage() + const vscode = useVSCode() + + const [name, setName] = createSignal(props.skill.name) + const [content, setContent] = createSignal(props.skill.content) + const [saving, setSaving] = createSignal(false) + const [error, setError] = createSignal(null) + + // Listen for save result + const unsubscribe = vscode.onMessage((msg) => { + if (msg.type === "skillEditResult") { + setSaving(false) + if (msg.success) { + props.onSave({ name: name(), content: content() }) + props.onClose() + } else { + setError(msg.error || "Failed to save skill") + } + } + }) + + onCleanup(unsubscribe) + + const handleSave = () => { + if (!name().trim()) { + setError("Skill name cannot be empty") + return + } + + setSaving(true) + setError(null) + + vscode.postMessage({ + type: "editSkill", + originalName: props.skill.name, + name: name(), + content: content(), + }) + } + + return ( + { + if (!open) props.onClose() + }} + > +
+

+ {language.t("settings.agentBehaviour.editSkill") || "Edit Skill"} +

+ + {/* Skill Name */} +
+ + setName(val)} + placeholder="Skill name" + disabled={saving()} + /> +
+ + {/* Skill Content */} +
+ +