Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 54ea34e

Browse files
brunobergherroomote[bot]roomote
authored
ux: improve Skills and Slash Commands settings UI with multi-mode support (#11157)
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com>
1 parent 460cff4 commit 54ea34e

59 files changed

Lines changed: 2240 additions & 1084 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/types/src/skills.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ export interface SkillMetadata {
77
description: string // Required: when to use this skill
88
path: string // Absolute path to SKILL.md (or "<built-in:name>" for built-in skills)
99
source: "global" | "project" | "built-in" // Where the skill was discovered
10-
mode?: string // If set, skill is only available in this mode
10+
/**
11+
* @deprecated Use modeSlugs instead. Kept for backward compatibility.
12+
* If set, skill is only available in this mode.
13+
*/
14+
mode?: string
15+
/**
16+
* Mode slugs where this skill is available.
17+
* - undefined or empty array means the skill is available in all modes ("Any mode").
18+
* - An array with one or more mode slugs restricts the skill to those modes.
19+
*/
20+
modeSlugs?: string[]
1121
}
1222

1323
/**

packages/types/src/vscode-extension-host.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ export interface WebviewMessage {
605605
| "createSkill"
606606
| "deleteSkill"
607607
| "moveSkill"
608+
| "updateSkillModes"
608609
| "openSkillFile"
609610
text?: string
610611
editedMessageContent?: string
@@ -641,9 +642,15 @@ export interface WebviewMessage {
641642
payload?: WebViewMessagePayload
642643
source?: "global" | "project" | "built-in"
643644
skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile)
645+
/** @deprecated Use skillModeSlugs instead */
644646
skillMode?: string // For skill operations (current mode restriction)
647+
/** @deprecated Use newSkillModeSlugs instead */
645648
newSkillMode?: string // For moveSkill (target mode)
646649
skillDescription?: string // For createSkill (skill description)
650+
/** Mode slugs for skill operations. undefined/empty = any mode */
651+
skillModeSlugs?: string[] // For skill operations (mode restrictions)
652+
/** Target mode slugs for updateSkillModes */
653+
newSkillModeSlugs?: string[] // For updateSkillModes (new mode restrictions)
647654
requestId?: string
648655
ids?: string[]
649656
hasSystemPromptOverride?: boolean

src/core/webview/__tests__/skillsMessageHandler.spec.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe("skillsMessageHandler", () => {
5252
const mockDeleteSkill = vi.fn()
5353
const mockMoveSkill = vi.fn()
5454
const mockGetSkill = vi.fn()
55+
const mockFindSkillByNameAndSource = vi.fn()
5556

5657
const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
5758
const skillsManager = hasSkillsManager
@@ -61,6 +62,7 @@ describe("skillsMessageHandler", () => {
6162
deleteSkill: mockDeleteSkill,
6263
moveSkill: mockMoveSkill,
6364
getSkill: mockGetSkill,
65+
findSkillByNameAndSource: mockFindSkillByNameAndSource,
6466
}
6567
: undefined
6668

@@ -158,7 +160,7 @@ describe("skillsMessageHandler", () => {
158160
} as WebviewMessage)
159161

160162
expect(result).toEqual(mockSkills)
161-
expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", "code")
163+
expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", ["code"])
162164
})
163165

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

360362
await handleOpenSkillFile(provider, {
361363
type: "openSkillFile",
362364
skillName: "test-skill",
363365
source: "global",
364366
} as WebviewMessage)
365367

366-
expect(mockGetSkill).toHaveBeenCalledWith("test-skill", "global", undefined)
368+
expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("test-skill", "global")
367369
expect(openFile).toHaveBeenCalledWith("/path/to/test-skill/SKILL.md")
368370
})
369371

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

374376
await handleOpenSkillFile(provider, {
375377
type: "openSkillFile",
@@ -378,7 +380,7 @@ describe("skillsMessageHandler", () => {
378380
skillMode: "code",
379381
} as WebviewMessage)
380382

381-
expect(mockGetSkill).toHaveBeenCalledWith("project-skill", "project", "code")
383+
expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("project-skill", "project")
382384
expect(openFile).toHaveBeenCalledWith("/project/.roo/skills/project-skill/SKILL.md")
383385
})
384386

@@ -416,7 +418,7 @@ describe("skillsMessageHandler", () => {
416418

417419
it("shows error when skill is not found", async () => {
418420
const provider = createMockProvider(true)
419-
mockGetSkill.mockReturnValue(undefined)
421+
mockFindSkillByNameAndSource.mockReturnValue(undefined)
420422

421423
await handleOpenSkillFile(provider, {
422424
type: "openSkillFile",

src/core/webview/skillsMessageHandler.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export async function handleCreateSkill(
3838
const skillName = message.skillName
3939
const source = message.source
4040
const skillDescription = message.skillDescription
41-
const skillMode = message.skillMode
41+
// Support new modeSlugs array or fall back to legacy skillMode
42+
const modeSlugs = message.skillModeSlugs ?? (message.skillMode ? [message.skillMode] : undefined)
4243

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

57-
const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, skillMode)
58+
const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, modeSlugs)
5859

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

8688
if (!skillName || !source) {
8789
throw new Error(t("skills:errors.missing_delete_fields"))
@@ -152,14 +154,53 @@ export async function handleMoveSkill(
152154
}
153155
}
154156

157+
/**
158+
* Handles the updateSkillModes message - updates the mode associations for a skill
159+
*/
160+
export async function handleUpdateSkillModes(
161+
provider: ClineProvider,
162+
message: WebviewMessage,
163+
): Promise<SkillMetadata[] | undefined> {
164+
try {
165+
const skillName = message.skillName
166+
const source = message.source
167+
const newModeSlugs = message.newSkillModeSlugs
168+
169+
if (!skillName || !source) {
170+
throw new Error(t("skills:errors.missing_update_modes_fields"))
171+
}
172+
173+
// Built-in skills cannot be modified
174+
if (source === "built-in") {
175+
throw new Error(t("skills:errors.cannot_modify_builtin"))
176+
}
177+
178+
const skillsManager = provider.getSkillsManager()
179+
if (!skillsManager) {
180+
throw new Error(t("skills:errors.manager_unavailable"))
181+
}
182+
183+
await skillsManager.updateSkillModes(skillName, source, newModeSlugs)
184+
185+
// Send updated skills list
186+
const skills = skillsManager.getSkillsMetadata()
187+
await provider.postMessageToWebview({ type: "skills", skills })
188+
return skills
189+
} catch (error) {
190+
const errorMessage = error instanceof Error ? error.message : String(error)
191+
provider.log(`Error updating skill modes: ${errorMessage}`)
192+
vscode.window.showErrorMessage(`Failed to update skill modes: ${errorMessage}`)
193+
return undefined
194+
}
195+
}
196+
155197
/**
156198
* Handles the openSkillFile message - opens a skill file in the editor
157199
*/
158200
export async function handleOpenSkillFile(provider: ClineProvider, message: WebviewMessage): Promise<void> {
159201
try {
160202
const skillName = message.skillName
161203
const source = message.source
162-
const skillMode = message.skillMode
163204

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

178-
const skill = skillsManager.getSkill(skillName, source, skillMode)
219+
// Find skill by name and source (skills may have modeSlugs arrays now)
220+
const skill = skillsManager.findSkillByNameAndSource(skillName, source)
179221
if (!skill) {
180222
throw new Error(t("skills:errors.skill_not_found", { name: skillName }))
181223
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
handleCreateSkill,
3838
handleDeleteSkill,
3939
handleMoveSkill,
40+
handleUpdateSkillModes,
4041
handleOpenSkillFile,
4142
} from "./skillsMessageHandler"
4243
import { changeLanguage, t } from "../../i18n"
@@ -2992,6 +2993,10 @@ export const webviewMessageHandler = async (
29922993
await handleMoveSkill(provider, message)
29932994
break
29942995
}
2996+
case "updateSkillModes": {
2997+
await handleUpdateSkillModes(provider, message)
2998+
break
2999+
}
29953000
case "openSkillFile": {
29963001
await handleOpenSkillFile(provider, message)
29973002
break

src/i18n/locales/ca/skills.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/de/skills.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/en/skills.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"not_found": "Skill \"{{name}}\" not found in {{source}}{{modeInfo}}",
99
"missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
1010
"missing_move_fields": "Missing required fields: skillName or source",
11+
"missing_update_modes_fields": "Missing required fields: skillName or source",
1112
"manager_unavailable": "Skills manager not available",
1213
"missing_delete_fields": "Missing required fields: skillName or source",
1314
"skill_not_found": "Skill \"{{name}}\" not found",

src/i18n/locales/es/skills.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/fr/skills.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)