Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
12 changes: 11 additions & 1 deletion packages/types/src/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ export interface SkillMetadata {
description: string // Required: when to use this skill
path: string // Absolute path to SKILL.md (or "<built-in:name>" for built-in skills)
source: "global" | "project" | "built-in" // Where the skill was discovered
mode?: string // If set, skill is only available in this mode
/**
* @deprecated Use modeSlugs instead. Kept for backward compatibility.
* If set, skill is only available in this mode.
*/
mode?: string
/**
* Mode slugs where this skill is available.
* - undefined or empty array means the skill is available in all modes ("Any mode").
* - An array with one or more mode slugs restricts the skill to those modes.
*/
modeSlugs?: string[]
}

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ export interface WebviewMessage {
| "createSkill"
| "deleteSkill"
| "moveSkill"
| "updateSkillModes"
| "openSkillFile"
text?: string
editedMessageContent?: string
Expand Down Expand Up @@ -641,9 +642,15 @@ export interface WebviewMessage {
payload?: WebViewMessagePayload
source?: "global" | "project" | "built-in"
skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile)
/** @deprecated Use skillModeSlugs instead */
skillMode?: string // For skill operations (current mode restriction)
/** @deprecated Use newSkillModeSlugs instead */
newSkillMode?: string // For moveSkill (target mode)
skillDescription?: string // For createSkill (skill description)
/** Mode slugs for skill operations. undefined/empty = any mode */
skillModeSlugs?: string[] // For skill operations (mode restrictions)
/** Target mode slugs for updateSkillModes */
newSkillModeSlugs?: string[] // For updateSkillModes (new mode restrictions)
requestId?: string
ids?: string[]
hasSystemPromptOverride?: boolean
Expand Down
14 changes: 8 additions & 6 deletions src/core/webview/__tests__/skillsMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("skillsMessageHandler", () => {
const mockDeleteSkill = vi.fn()
const mockMoveSkill = vi.fn()
const mockGetSkill = vi.fn()
const mockFindSkillByNameAndSource = vi.fn()

const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
const skillsManager = hasSkillsManager
Expand All @@ -61,6 +62,7 @@ describe("skillsMessageHandler", () => {
deleteSkill: mockDeleteSkill,
moveSkill: mockMoveSkill,
getSkill: mockGetSkill,
findSkillByNameAndSource: mockFindSkillByNameAndSource,
}
: undefined

Expand Down Expand Up @@ -158,7 +160,7 @@ describe("skillsMessageHandler", () => {
} as WebviewMessage)

expect(result).toEqual(mockSkills)
expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", "code")
expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", ["code"])
})

it("returns undefined when required fields are missing", async () => {
Expand Down Expand Up @@ -355,21 +357,21 @@ describe("skillsMessageHandler", () => {
describe("handleOpenSkillFile", () => {
it("opens a skill file successfully", async () => {
const provider = createMockProvider(true)
mockGetSkill.mockReturnValue(mockSkills[0])
mockFindSkillByNameAndSource.mockReturnValue(mockSkills[0])

await handleOpenSkillFile(provider, {
type: "openSkillFile",
skillName: "test-skill",
source: "global",
} as WebviewMessage)

expect(mockGetSkill).toHaveBeenCalledWith("test-skill", "global", undefined)
expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("test-skill", "global")
expect(openFile).toHaveBeenCalledWith("/path/to/test-skill/SKILL.md")
})

it("opens a skill file with mode restriction", async () => {
const provider = createMockProvider(true)
mockGetSkill.mockReturnValue(mockSkills[1])
mockFindSkillByNameAndSource.mockReturnValue(mockSkills[1])

await handleOpenSkillFile(provider, {
type: "openSkillFile",
Expand All @@ -378,7 +380,7 @@ describe("skillsMessageHandler", () => {
skillMode: "code",
} as WebviewMessage)

expect(mockGetSkill).toHaveBeenCalledWith("project-skill", "project", "code")
expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("project-skill", "project")
expect(openFile).toHaveBeenCalledWith("/project/.roo/skills/project-skill/SKILL.md")
})

Expand Down Expand Up @@ -416,7 +418,7 @@ describe("skillsMessageHandler", () => {

it("shows error when skill is not found", async () => {
const provider = createMockProvider(true)
mockGetSkill.mockReturnValue(undefined)
mockFindSkillByNameAndSource.mockReturnValue(undefined)

await handleOpenSkillFile(provider, {
type: "openSkillFile",
Expand Down
52 changes: 47 additions & 5 deletions src/core/webview/skillsMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export async function handleCreateSkill(
const skillName = message.skillName
const source = message.source
const skillDescription = message.skillDescription
const skillMode = message.skillMode
// Support new modeSlugs array or fall back to legacy skillMode
const modeSlugs = message.skillModeSlugs ?? (message.skillMode ? [message.skillMode] : undefined)

if (!skillName || !source || !skillDescription) {
throw new Error(t("skills:errors.missing_create_fields"))
Expand All @@ -54,7 +55,7 @@ export async function handleCreateSkill(
throw new Error(t("skills:errors.manager_unavailable"))
}

const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, skillMode)
const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, modeSlugs)

// Open the created file in the editor
openFile(createdPath)
Expand All @@ -81,7 +82,8 @@ export async function handleDeleteSkill(
try {
const skillName = message.skillName
const source = message.source
const skillMode = message.skillMode
// Support new skillModeSlugs array or fall back to legacy skillMode
const skillMode = message.skillModeSlugs?.[0] ?? message.skillMode

if (!skillName || !source) {
throw new Error(t("skills:errors.missing_delete_fields"))
Expand Down Expand Up @@ -152,14 +154,53 @@ export async function handleMoveSkill(
}
}

/**
* Handles the updateSkillModes message - updates the mode associations for a skill
*/
export async function handleUpdateSkillModes(
provider: ClineProvider,
message: WebviewMessage,
): Promise<SkillMetadata[] | undefined> {
try {
const skillName = message.skillName
const source = message.source
const newModeSlugs = message.newSkillModeSlugs

if (!skillName || !source) {
throw new Error(t("skills:errors.missing_update_modes_fields"))
}

// Built-in skills cannot be modified
if (source === "built-in") {
throw new Error(t("skills:errors.cannot_modify_builtin"))
}

const skillsManager = provider.getSkillsManager()
if (!skillsManager) {
throw new Error(t("skills:errors.manager_unavailable"))
}

await skillsManager.updateSkillModes(skillName, source, newModeSlugs)

// Send updated skills list
const skills = skillsManager.getSkillsMetadata()
await provider.postMessageToWebview({ type: "skills", skills })
return skills
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
provider.log(`Error updating skill modes: ${errorMessage}`)
vscode.window.showErrorMessage(`Failed to update skill modes: ${errorMessage}`)
return undefined
}
}

/**
* Handles the openSkillFile message - opens a skill file in the editor
*/
export async function handleOpenSkillFile(provider: ClineProvider, message: WebviewMessage): Promise<void> {
try {
const skillName = message.skillName
const source = message.source
const skillMode = message.skillMode

if (!skillName || !source) {
throw new Error(t("skills:errors.missing_delete_fields"))
Expand All @@ -175,7 +216,8 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv
throw new Error(t("skills:errors.manager_unavailable"))
}

const skill = skillsManager.getSkill(skillName, source, skillMode)
// Find skill by name and source (skills may have modeSlugs arrays now)
const skill = skillsManager.findSkillByNameAndSource(skillName, source)
if (!skill) {
throw new Error(t("skills:errors.skill_not_found", { name: skillName }))
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
handleCreateSkill,
handleDeleteSkill,
handleMoveSkill,
handleUpdateSkillModes,
handleOpenSkillFile,
} from "./skillsMessageHandler"
import { changeLanguage, t } from "../../i18n"
Expand Down Expand Up @@ -2992,6 +2993,10 @@ export const webviewMessageHandler = async (
await handleMoveSkill(provider, message)
break
}
case "updateSkillModes": {
await handleUpdateSkillModes(provider, message)
break
}
case "openSkillFile": {
await handleOpenSkillFile(provider, message)
break
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ca/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/de/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/en/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"not_found": "Skill \"{{name}}\" not found in {{source}}{{modeInfo}}",
"missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
"missing_move_fields": "Missing required fields: skillName or source",
"missing_update_modes_fields": "Missing required fields: skillName or source",
"manager_unavailable": "Skills manager not available",
"missing_delete_fields": "Missing required fields: skillName or source",
"skill_not_found": "Skill \"{{name}}\" not found",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/fr/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/hi/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/id/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/it/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ja/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ko/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/nl/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pl/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ru/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/tr/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/vi/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/zh-CN/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/zh-TW/skills.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading