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
6 changes: 4 additions & 2 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ export interface WebviewMessage {
| "requestSkills"
| "createSkill"
| "deleteSkill"
| "moveSkill"
| "openSkillFile"
text?: string
editedMessageContent?: string
Expand Down Expand Up @@ -639,8 +640,9 @@ export interface WebviewMessage {
timeout?: number
payload?: WebViewMessagePayload
source?: "global" | "project" | "built-in"
skillName?: string // For skill operations (createSkill, deleteSkill, openSkillFile)
skillMode?: string // For skill operations (mode restriction)
skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile)
skillMode?: string // For skill operations (current mode restriction)
newSkillMode?: string // For moveSkill (target mode)
skillDescription?: string // For createSkill (skill description)
requestId?: string
ids?: string[]
Expand Down
101 changes: 100 additions & 1 deletion src/core/webview/__tests__/skillsMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,31 @@ vi.mock("../../../i18n", () => ({
"skills:errors.missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
"skills:errors.manager_unavailable": "Skills manager not available",
"skills:errors.missing_delete_fields": "Missing required fields: skillName or source",
"skills:errors.missing_move_fields": "Missing required fields: skillName or source",
"skills:errors.skill_not_found": `Skill "${params?.name}" not found`,
"skills:errors.cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
}
return translations[key] || key
},
}))

import * as vscode from "vscode"
import { openFile } from "../../../integrations/misc/open-file"
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "../skillsMessageHandler"
import {
handleRequestSkills,
handleCreateSkill,
handleDeleteSkill,
handleMoveSkill,
handleOpenSkillFile,
} from "../skillsMessageHandler"

describe("skillsMessageHandler", () => {
const mockLog = vi.fn()
const mockPostMessageToWebview = vi.fn()
const mockGetSkillsMetadata = vi.fn()
const mockCreateSkill = vi.fn()
const mockDeleteSkill = vi.fn()
const mockMoveSkill = vi.fn()
const mockGetSkill = vi.fn()

const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
Expand All @@ -50,6 +59,7 @@ describe("skillsMessageHandler", () => {
getSkillsMetadata: mockGetSkillsMetadata,
createSkill: mockCreateSkill,
deleteSkill: mockDeleteSkill,
moveSkill: mockMoveSkill,
getSkill: mockGetSkill,
}
: undefined
Expand Down Expand Up @@ -253,6 +263,95 @@ describe("skillsMessageHandler", () => {
})
})

describe("handleMoveSkill", () => {
it("moves a skill successfully", async () => {
const provider = createMockProvider(true)
mockMoveSkill.mockResolvedValue(undefined)
mockGetSkillsMetadata.mockReturnValue([mockSkills[0]])

const result = await handleMoveSkill(provider, {
type: "moveSkill",
skillName: "test-skill",
source: "global",
skillMode: undefined,
newSkillMode: "code",
} as WebviewMessage)

expect(result).toEqual([mockSkills[0]])
expect(mockMoveSkill).toHaveBeenCalledWith("test-skill", "global", undefined, "code")
expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [mockSkills[0]] })
})

it("moves a skill from one mode to another", async () => {
const provider = createMockProvider(true)
mockMoveSkill.mockResolvedValue(undefined)
mockGetSkillsMetadata.mockReturnValue([mockSkills[1]])

const result = await handleMoveSkill(provider, {
type: "moveSkill",
skillName: "project-skill",
source: "project",
skillMode: "code",
newSkillMode: "architect",
} as WebviewMessage)

expect(result).toEqual([mockSkills[1]])
expect(mockMoveSkill).toHaveBeenCalledWith("project-skill", "project", "code", "architect")
})

it("returns undefined when required fields are missing", async () => {
const provider = createMockProvider(true)

const result = await handleMoveSkill(provider, {
type: "moveSkill",
skillName: "test-skill",
// missing source
} as WebviewMessage)

expect(result).toBeUndefined()
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Missing required fields: skillName or source")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Failed to move skill: Missing required fields: skillName or source",
)
})

it("returns undefined when skills manager is not available", async () => {
const provider = createMockProvider(false)

const result = await handleMoveSkill(provider, {
type: "moveSkill",
skillName: "test-skill",
source: "global",
newSkillMode: "code",
} as WebviewMessage)

expect(result).toBeUndefined()
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Skills manager not available")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Failed to move skill: Skills manager not available",
)
})

it("returns undefined when trying to move a built-in skill", async () => {
const provider = createMockProvider(true)

const result = await handleMoveSkill(provider, {
type: "moveSkill",
skillName: "test-skill",
source: "built-in",
newSkillMode: "code",
} as WebviewMessage)

expect(result).toBeUndefined()
expect(mockLog).toHaveBeenCalledWith(
"Error moving skill: Built-in skills cannot be created, deleted, or moved",
)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Failed to move skill: Built-in skills cannot be created, deleted, or moved",
)
})
})

describe("handleOpenSkillFile", () => {
it("opens a skill file successfully", async () => {
const provider = createMockProvider(true)
Expand Down
41 changes: 41 additions & 0 deletions src/core/webview/skillsMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,47 @@ export async function handleDeleteSkill(
}
}

/**
* Handles the moveSkill message - moves a skill to a different mode
*/
export async function handleMoveSkill(
provider: ClineProvider,
message: WebviewMessage,
): Promise<SkillMetadata[] | undefined> {
try {
const skillName = message.skillName
const source = message.source
const currentMode = message.skillMode
const newMode = message.newSkillMode

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

// Built-in skills cannot be moved
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.moveSkill(skillName, source, currentMode, newMode)

// 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 moving skill: ${errorMessage}`)
vscode.window.showErrorMessage(`Failed to move skill: ${errorMessage}`)
return undefined
}
}

/**
* Handles the openSkillFile message - opens a skill file in the editor
*/
Expand Down
12 changes: 11 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ import { ClineProvider } from "./ClineProvider"
import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
import { generateErrorDiagnostics } from "./diagnosticsHandler"
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "./skillsMessageHandler"
import {
handleRequestSkills,
handleCreateSkill,
handleDeleteSkill,
handleMoveSkill,
handleOpenSkillFile,
} from "./skillsMessageHandler"
import { changeLanguage, t } from "../../i18n"
import { Package } from "../../shared/package"
import { type RouterName, toRouterName } from "../../shared/api"
Expand Down Expand Up @@ -2982,6 +2988,10 @@ export const webviewMessageHandler = async (
await handleDeleteSkill(provider, message)
break
}
case "moveSkill": {
await handleMoveSkill(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.

3 changes: 2 additions & 1 deletion src/i18n/locales/en/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
"already_exists": "Skill \"{{name}}\" already exists at {{path}}",
"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",
"manager_unavailable": "Skills manager not available",
"missing_delete_fields": "Missing required fields: skillName or source",
"skill_not_found": "Skill \"{{name}}\" not found",
"cannot_modify_builtin": "Built-in skills cannot be created or deleted",
"cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
"cannot_open_builtin": "Built-in skills cannot be opened as files"
}
}
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