Skip to content

Commit 16fbabf

Browse files
SannidhyaSahSannidhya
andauthored
feat: add mode dropdown to change skill mode dynamically (#10513) (#11102)
Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local>
1 parent 3400499 commit 16fbabf

44 files changed

Lines changed: 895 additions & 33 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/vscode-extension-host.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ export interface WebviewMessage {
604604
| "requestSkills"
605605
| "createSkill"
606606
| "deleteSkill"
607+
| "moveSkill"
607608
| "openSkillFile"
608609
text?: string
609610
editedMessageContent?: string
@@ -639,8 +640,9 @@ export interface WebviewMessage {
639640
timeout?: number
640641
payload?: WebViewMessagePayload
641642
source?: "global" | "project" | "built-in"
642-
skillName?: string // For skill operations (createSkill, deleteSkill, openSkillFile)
643-
skillMode?: string // For skill operations (mode restriction)
643+
skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile)
644+
skillMode?: string // For skill operations (current mode restriction)
645+
newSkillMode?: string // For moveSkill (target mode)
644646
skillDescription?: string // For createSkill (skill description)
645647
requestId?: string
646648
ids?: string[]

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

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,31 @@ vi.mock("../../../i18n", () => ({
2626
"skills:errors.missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
2727
"skills:errors.manager_unavailable": "Skills manager not available",
2828
"skills:errors.missing_delete_fields": "Missing required fields: skillName or source",
29+
"skills:errors.missing_move_fields": "Missing required fields: skillName or source",
2930
"skills:errors.skill_not_found": `Skill "${params?.name}" not found`,
31+
"skills:errors.cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
3032
}
3133
return translations[key] || key
3234
},
3335
}))
3436

3537
import * as vscode from "vscode"
3638
import { openFile } from "../../../integrations/misc/open-file"
37-
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "../skillsMessageHandler"
39+
import {
40+
handleRequestSkills,
41+
handleCreateSkill,
42+
handleDeleteSkill,
43+
handleMoveSkill,
44+
handleOpenSkillFile,
45+
} from "../skillsMessageHandler"
3846

3947
describe("skillsMessageHandler", () => {
4048
const mockLog = vi.fn()
4149
const mockPostMessageToWebview = vi.fn()
4250
const mockGetSkillsMetadata = vi.fn()
4351
const mockCreateSkill = vi.fn()
4452
const mockDeleteSkill = vi.fn()
53+
const mockMoveSkill = vi.fn()
4554
const mockGetSkill = vi.fn()
4655

4756
const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
@@ -50,6 +59,7 @@ describe("skillsMessageHandler", () => {
5059
getSkillsMetadata: mockGetSkillsMetadata,
5160
createSkill: mockCreateSkill,
5261
deleteSkill: mockDeleteSkill,
62+
moveSkill: mockMoveSkill,
5363
getSkill: mockGetSkill,
5464
}
5565
: undefined
@@ -253,6 +263,95 @@ describe("skillsMessageHandler", () => {
253263
})
254264
})
255265

266+
describe("handleMoveSkill", () => {
267+
it("moves a skill successfully", async () => {
268+
const provider = createMockProvider(true)
269+
mockMoveSkill.mockResolvedValue(undefined)
270+
mockGetSkillsMetadata.mockReturnValue([mockSkills[0]])
271+
272+
const result = await handleMoveSkill(provider, {
273+
type: "moveSkill",
274+
skillName: "test-skill",
275+
source: "global",
276+
skillMode: undefined,
277+
newSkillMode: "code",
278+
} as WebviewMessage)
279+
280+
expect(result).toEqual([mockSkills[0]])
281+
expect(mockMoveSkill).toHaveBeenCalledWith("test-skill", "global", undefined, "code")
282+
expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [mockSkills[0]] })
283+
})
284+
285+
it("moves a skill from one mode to another", async () => {
286+
const provider = createMockProvider(true)
287+
mockMoveSkill.mockResolvedValue(undefined)
288+
mockGetSkillsMetadata.mockReturnValue([mockSkills[1]])
289+
290+
const result = await handleMoveSkill(provider, {
291+
type: "moveSkill",
292+
skillName: "project-skill",
293+
source: "project",
294+
skillMode: "code",
295+
newSkillMode: "architect",
296+
} as WebviewMessage)
297+
298+
expect(result).toEqual([mockSkills[1]])
299+
expect(mockMoveSkill).toHaveBeenCalledWith("project-skill", "project", "code", "architect")
300+
})
301+
302+
it("returns undefined when required fields are missing", async () => {
303+
const provider = createMockProvider(true)
304+
305+
const result = await handleMoveSkill(provider, {
306+
type: "moveSkill",
307+
skillName: "test-skill",
308+
// missing source
309+
} as WebviewMessage)
310+
311+
expect(result).toBeUndefined()
312+
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Missing required fields: skillName or source")
313+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
314+
"Failed to move skill: Missing required fields: skillName or source",
315+
)
316+
})
317+
318+
it("returns undefined when skills manager is not available", async () => {
319+
const provider = createMockProvider(false)
320+
321+
const result = await handleMoveSkill(provider, {
322+
type: "moveSkill",
323+
skillName: "test-skill",
324+
source: "global",
325+
newSkillMode: "code",
326+
} as WebviewMessage)
327+
328+
expect(result).toBeUndefined()
329+
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Skills manager not available")
330+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
331+
"Failed to move skill: Skills manager not available",
332+
)
333+
})
334+
335+
it("returns undefined when trying to move a built-in skill", async () => {
336+
const provider = createMockProvider(true)
337+
338+
const result = await handleMoveSkill(provider, {
339+
type: "moveSkill",
340+
skillName: "test-skill",
341+
source: "built-in",
342+
newSkillMode: "code",
343+
} as WebviewMessage)
344+
345+
expect(result).toBeUndefined()
346+
expect(mockLog).toHaveBeenCalledWith(
347+
"Error moving skill: Built-in skills cannot be created, deleted, or moved",
348+
)
349+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
350+
"Failed to move skill: Built-in skills cannot be created, deleted, or moved",
351+
)
352+
})
353+
})
354+
256355
describe("handleOpenSkillFile", () => {
257356
it("opens a skill file successfully", async () => {
258357
const provider = createMockProvider(true)

src/core/webview/skillsMessageHandler.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,47 @@ export async function handleDeleteSkill(
111111
}
112112
}
113113

114+
/**
115+
* Handles the moveSkill message - moves a skill to a different mode
116+
*/
117+
export async function handleMoveSkill(
118+
provider: ClineProvider,
119+
message: WebviewMessage,
120+
): Promise<SkillMetadata[] | undefined> {
121+
try {
122+
const skillName = message.skillName
123+
const source = message.source
124+
const currentMode = message.skillMode
125+
const newMode = message.newSkillMode
126+
127+
if (!skillName || !source) {
128+
throw new Error(t("skills:errors.missing_move_fields"))
129+
}
130+
131+
// Built-in skills cannot be moved
132+
if (source === "built-in") {
133+
throw new Error(t("skills:errors.cannot_modify_builtin"))
134+
}
135+
136+
const skillsManager = provider.getSkillsManager()
137+
if (!skillsManager) {
138+
throw new Error(t("skills:errors.manager_unavailable"))
139+
}
140+
141+
await skillsManager.moveSkill(skillName, source, currentMode, newMode)
142+
143+
// Send updated skills list
144+
const skills = skillsManager.getSkillsMetadata()
145+
await provider.postMessageToWebview({ type: "skills", skills })
146+
return skills
147+
} catch (error) {
148+
const errorMessage = error instanceof Error ? error.message : String(error)
149+
provider.log(`Error moving skill: ${errorMessage}`)
150+
vscode.window.showErrorMessage(`Failed to move skill: ${errorMessage}`)
151+
return undefined
152+
}
153+
}
154+
114155
/**
115156
* Handles the openSkillFile message - opens a skill file in the editor
116157
*/

src/core/webview/webviewMessageHandler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ import { ClineProvider } from "./ClineProvider"
3232
import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
3333
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
3434
import { generateErrorDiagnostics } from "./diagnosticsHandler"
35-
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "./skillsMessageHandler"
35+
import {
36+
handleRequestSkills,
37+
handleCreateSkill,
38+
handleDeleteSkill,
39+
handleMoveSkill,
40+
handleOpenSkillFile,
41+
} from "./skillsMessageHandler"
3642
import { changeLanguage, t } from "../../i18n"
3743
import { Package } from "../../shared/package"
3844
import { type RouterName, toRouterName } from "../../shared/api"
@@ -2982,6 +2988,10 @@ export const webviewMessageHandler = async (
29822988
await handleDeleteSkill(provider, message)
29832989
break
29842990
}
2991+
case "moveSkill": {
2992+
await handleMoveSkill(provider, message)
2993+
break
2994+
}
29852995
case "openSkillFile": {
29862996
await handleOpenSkillFile(provider, message)
29872997
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
"already_exists": "Skill \"{{name}}\" already exists at {{path}}",
88
"not_found": "Skill \"{{name}}\" not found in {{source}}{{modeInfo}}",
99
"missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
10+
"missing_move_fields": "Missing required fields: skillName or source",
1011
"manager_unavailable": "Skills manager not available",
1112
"missing_delete_fields": "Missing required fields: skillName or source",
1213
"skill_not_found": "Skill \"{{name}}\" not found",
13-
"cannot_modify_builtin": "Built-in skills cannot be created or deleted",
14+
"cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
1415
"cannot_open_builtin": "Built-in skills cannot be opened as files"
1516
}
1617
}

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.

src/i18n/locales/hi/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)