diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index c30299a5cdf..818f894fc4d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -806,6 +806,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "updateConfig": await this.handleUpdateConfig(message.config) break + case "editSkill": + await this.handleEditSkill(message.originalName, message.name, message.content) + break case "setLanguage": await vscode.workspace .getConfiguration("kilo-code.new") @@ -2292,6 +2295,60 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + /** + * Handle skill editing request from the webview. + * Writes the updated skill content to the SKILL.md file. + */ + private async handleEditSkill(originalName: string, name: string, content: string): Promise { + if (!this.client || this.connectionState !== "connected") { + this.postMessage({ + type: "skillEditResult", + success: false, + error: "Not connected to CLI backend", + }) + return + } + + try { + const dir = this.getWorkspaceDirectory() + + // Get the skill location from the CLI + const { data: skills } = await this.client.app.skills( + { directory: dir }, + { throwOnError: true }, + ) + + const skill = skills.find((s) => s.name === originalName) + if (!skill) { + this.postMessage({ + type: "skillEditResult", + success: false, + error: `Skill "${originalName}" not found`, + }) + return + } + + const skillUri = vscode.Uri.file(skill.location) + const skillContent = Buffer.from(content, "utf8") + + // Write the updated skill content + await vscode.workspace.fs.writeFile(skillUri, skillContent) + + // Send success message + this.postMessage({ + type: "skillEditResult", + success: true, + }) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to edit skill:", error) + this.postMessage({ + type: "skillEditResult", + success: false, + error: getErrorMessage(error) || "Failed to save skill", + }) + } + } + /** * Ensure a session exists, creating one if needed. Returns the resolved * session ID and workspace directory, or undefined when the client is 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..be53f42dc70 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SkillEditModal.tsx @@ -0,0 +1,154 @@ +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 +} + +const getInitialSkillContent = (skill: SkillInfo): string => { + const skillWithContent = skill as SkillInfo & { content?: unknown } + return typeof skillWithContent.content === "string" ? skillWithContent.content : "" +} + +export const SkillEditModal = (props: Props) => { + const language = useLanguage() + const vscode = useVSCode() + + const [name, setName] = createSignal(props.skill.name) + const [content, setContent] = createSignal(getInitialSkillContent(props.skill)) + 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 */} +
+ +