Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/skills-tab-agent-behaviour.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Add Skills tab to Agent Behaviour settings for viewing and managing installed skills
5 changes: 5 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,11 @@ export class ClineProvider
})
}
}

async postSkillsDataToWebview() {
const skills = this.skillsManager?.getAllSkills() ?? []
this.postMessageToWebview({ type: "skillsData", skills })
}
// kilocode_change end

/**
Expand Down
20 changes: 20 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,13 @@ export const webviewMessageHandler = async (
break
}

// kilocode_change start
case "refreshSkills": {
await provider.postSkillsDataToWebview()
break
}
// kilocode_change end

case "toggleRule": {
if (message.rulePath && typeof message.enabled === "boolean" && typeof message.isGlobal === "boolean") {
await toggleRule(
Expand Down Expand Up @@ -3684,6 +3691,19 @@ export const webviewMessageHandler = async (
if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
try {
await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)

// kilocode_change start: Force skills refresh after skill deletion
// If the removed item is a skill, force a refresh of the SkillsManager
// to ensure the cache is updated before sending data to the webview
if (message.mpItem.type === "skill") {
const skillsManager = provider.getSkillsManager()
if (skillsManager) {
await skillsManager.discoverSkills()
}
await provider.postSkillsDataToWebview()
}
// kilocode_change end

await provider.postStateToWebview()

// Send success message to webview
Expand Down
3 changes: 3 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export interface ExtensionMessage {
| "indexCleared"
| "codebaseIndexConfig"
| "rulesData" // kilocode_change
| "skillsData" // kilocode_change
| "marketplaceInstallResult"
| "marketplaceRemoveResult"
| "marketplaceData"
Expand Down Expand Up @@ -313,6 +314,7 @@ export interface ExtensionMessage {
localRules?: ClineRulesToggles
globalWorkflows?: ClineRulesToggles
localWorkflows?: ClineRulesToggles
skills?: Array<{ name: string; description: string; path: string; source: "global" | "project"; mode?: string }> // kilocode_change
marketplaceItems?: MarketplaceItem[]
organizationMcps?: MarketplaceItem[]
marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
Expand Down Expand Up @@ -745,6 +747,7 @@ export interface WebviewMessage {
| "requestModes"
| "switchMode"
| "debugSetting"
| "refreshSkills" // kilocode_change
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export interface WebviewMessage {
| "condense" // kilocode_change
| "toggleWorkflow" // kilocode_change
| "refreshRules" // kilocode_change
| "refreshSkills" // kilocode_change
| "toggleRule" // kilocode_change
| "createRuleFile" // kilocode_change
| "deleteRuleFile" // kilocode_change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import BottomButton from "../BottomButton"
import KiloRulesWorkflowsView from "./KiloRulesWorkflowsView"
import ModesView from "@src/components/modes/ModesView"
import McpView from "@src/components/mcp/McpView"
import InstalledSkillsView from "@src/components/kilocode/settings/InstalledSkillsView"

const KiloRulesToggleModal: React.FC = () => {
const { t } = useTranslation()
Expand All @@ -18,6 +19,7 @@ const KiloRulesToggleModal: React.FC = () => {
t("kilocode:rules.agentBehaviourTypes.workflows"),
t("kilocode:rules.agentBehaviourTypes.mcps"),
t("kilocode:rules.agentBehaviourTypes.modes"),
t("kilocode:rules.agentBehaviourTypes.skills"),
].join(", ")

const [isVisible, setIsVisible] = useState(false)
Expand All @@ -26,7 +28,7 @@ const KiloRulesToggleModal: React.FC = () => {
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
const [currentView, setCurrentView] = useState<"modes" | "mcp" | "rule" | "workflow">("rule")
const [currentView, setCurrentView] = useState<"modes" | "mcp" | "rule" | "workflow" | "skills">("rule")

useClickAway(modalRef, () => {
setIsVisible(false)
Expand Down Expand Up @@ -109,6 +111,11 @@ const KiloRulesToggleModal: React.FC = () => {
onClick={() => setCurrentView("workflow")}>
{t("kilocode:rules.tabs.workflows")}
</StyledTabButton>
<StyledTabButton
$isActive={currentView === "skills"}
onClick={() => setCurrentView("skills")}>
{t("kilocode:settings.sections.skills")}
</StyledTabButton>
</div>
</div>

Expand All @@ -124,6 +131,7 @@ const KiloRulesToggleModal: React.FC = () => {
{currentView === "mcp" && <McpView hideHeader onDone={() => {}} />}
{currentView === "rule" && <KiloRulesWorkflowsView type="rule" />}
{currentView === "workflow" && <KiloRulesWorkflowsView type="workflow" />}
{currentView === "skills" && <InstalledSkillsView />}
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Section } from "@src/components/settings/Section"
import ModesView from "@src/components/modes/ModesView"
import McpView from "@src/components/mcp/McpView"
import KiloRulesWorkflowsView from "@src/components/kilocode/rules/KiloRulesWorkflowsView"
import InstalledSkillsView from "@src/components/kilocode/settings/InstalledSkillsView"

const StyledTabButton = styled.button<{ isActive: boolean }>`
background: none;
Expand Down Expand Up @@ -40,7 +41,7 @@ const TabButton = ({
)

const AgentBehaviourView = () => {
const [activeTab, setActiveTab] = useState<"modes" | "mcp" | "rules" | "workflows">("modes")
const [activeTab, setActiveTab] = useState<"modes" | "mcp" | "rules" | "workflows" | "skills">("modes")
const { t } = useAppTranslation()

return (
Expand Down Expand Up @@ -73,6 +74,9 @@ const AgentBehaviourView = () => {
<TabButton isActive={activeTab === "workflows"} onClick={() => setActiveTab("workflows")}>
{t("kilocode:rules.tabs.workflows")}
</TabButton>
<TabButton isActive={activeTab === "skills"} onClick={() => setActiveTab("skills")}>
{t("kilocode:settings.sections.skills")}
</TabButton>
</div>

{/* Content */}
Expand All @@ -81,6 +85,7 @@ const AgentBehaviourView = () => {
{activeTab === "mcp" && <McpView hideHeader onDone={() => {}} />}
{activeTab === "rules" && <KiloRulesWorkflowsView type="rule" />}
{activeTab === "workflows" && <KiloRulesWorkflowsView type="workflow" />}
{activeTab === "skills" && <InstalledSkillsView />}
</div>
</Section>
</div>
Expand Down
190 changes: 190 additions & 0 deletions webview-ui/src/components/kilocode/settings/InstalledSkillsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// kilocode_change - new file
import { useState, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"

import { vscode } from "@/utils/vscode"
import {
Button,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@src/components/ui"

interface SkillMetadata {
name: string
description: string
path: string
source: "global" | "project"
mode?: string
}

const InstalledSkillsView = () => {
const { t } = useTranslation()
const [skills, setSkills] = useState<SkillMetadata[]>([])
const [skillToDelete, setSkillToDelete] = useState<SkillMetadata | null>(null)

useEffect(() => {
// Request skills data on mount
vscode.postMessage({ type: "refreshSkills" })
}, [])

useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "skillsData") {
setSkills(message.skills ?? [])
}
}

window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [])

const handleDelete = (skill: SkillMetadata) => {
setSkillToDelete(skill)
}

const confirmDelete = () => {
if (!skillToDelete) return

vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: {
type: "skill",
id: skillToDelete.name,
name: skillToDelete.name,
description: skillToDelete.description,
category: "",
githubUrl: "",
content: "",
displayName: skillToDelete.name,
displayCategory: "",
},
mpInstallOptions: { target: skillToDelete.source },
})
setSkillToDelete(null)
}

const globalSkills = skills.filter((s) => s.source === "global")
const projectSkills = skills.filter((s) => s.source === "project")

return (
<div className="px-5">
<div className="text-xs text-[var(--vscode-descriptionForeground)] mb-4">
<p>
{t("kilocode:skills.description")}{" "}
<VSCodeLink
href="https://kilo.ai/docs/features/skills"
style={{ display: "inline" }}
className="text-xs">
{t("kilocode:docs")}
</VSCodeLink>
</p>
</div>

{skills.length === 0 ? (
<div className="text-sm text-[var(--vscode-descriptionForeground)] py-4">
{t("kilocode:skills.noSkills")}
</div>
) : (
<>
{/* Project Skills */}
{projectSkills.length > 0 && (
<SkillsSection
title={t("kilocode:skills.projectSkills")}
skills={projectSkills}
onDelete={handleDelete}
/>
)}

{/* Global Skills */}
{globalSkills.length > 0 && (
<SkillsSection
title={t("kilocode:skills.globalSkills")}
skills={globalSkills}
onDelete={handleDelete}
/>
)}
</>
)}

{/* Delete Confirmation Dialog */}
<Dialog open={!!skillToDelete} onOpenChange={(open) => !open && setSkillToDelete(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("kilocode:skills.deleteDialog.title")}</DialogTitle>
<DialogDescription>
{t("kilocode:skills.deleteDialog.description", { skillName: skillToDelete?.name })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="secondary" onClick={() => setSkillToDelete(null)}>
{t("kilocode:skills.deleteDialog.cancel")}
</Button>
<Button variant="primary" onClick={confirmDelete}>
{t("kilocode:skills.deleteDialog.delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

interface SkillsSectionProps {
title: string
skills: SkillMetadata[]
onDelete: (skill: SkillMetadata) => void
}

const SkillsSection = ({ title, skills, onDelete }: SkillsSectionProps) => {
return (
<div className="mb-4">
<h4 className="text-sm font-medium text-[var(--vscode-foreground)] mb-2">{title}</h4>
<div className="flex flex-col gap-2">
{skills.map((skill) => (
<SkillRow key={`${skill.source}-${skill.name}`} skill={skill} onDelete={onDelete} />
))}
</div>
</div>
)
}

interface SkillRowProps {
skill: SkillMetadata
onDelete: (skill: SkillMetadata) => void
}

const SkillRow = ({ skill, onDelete }: SkillRowProps) => {
return (
<div
className="flex items-center justify-between p-2 rounded"
style={{ background: "var(--vscode-textCodeBlock-background)" }}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--vscode-foreground)]">{skill.name}</span>
{skill.mode && (
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
background: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
}}>
{skill.mode}
</span>
)}
</div>
<div className="text-xs text-[var(--vscode-descriptionForeground)] truncate">{skill.description}</div>
</div>
<Button variant="ghost" size="icon" onClick={() => onDelete(skill)} style={{ marginLeft: "8px" }}>
<span className="codicon codicon-trash" style={{ fontSize: "14px" }}></span>
</Button>
</div>
)
}

export default InstalledSkillsView
Loading