From 9dc53c6ad07c6c1e89cebf49acf1f4a3ef112da0 Mon Sep 17 00:00:00 2001 From: Sannidhya Date: Fri, 30 Jan 2026 08:49:15 +0530 Subject: [PATCH 1/3] feat(skills): add mode dropdown to change skill mode dynamically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add moveSkill() method to SkillsManager that moves skills between mode directories - Replace static mode badge with interactive dropdown in SkillItem component - Automatically clean up empty mode directories after moving skills - Add comprehensive tests for move operation (42 backend tests, 21 message handler tests, 16 UI tests) - Built-in skills show static badge and have dropdown disabled - Support moving between: generic ↔ mode-specific, mode A ↔ mode B Fixes the issue where users couldn't change which mode a skill applies to after creation. --- packages/types/src/vscode-extension-host.ts | 6 +- .../__tests__/skillsMessageHandler.spec.ts | 101 ++++- src/core/webview/skillsMessageHandler.ts | 41 ++ src/core/webview/webviewMessageHandler.ts | 12 +- src/i18n/locales/en/skills.json | 3 +- src/services/skills/SkillsManager.ts | 72 ++++ .../skills/__tests__/SkillsManager.spec.ts | 406 ++++++++++++++++++ .../src/components/settings/SkillItem.tsx | 97 ++++- .../settings/__tests__/SkillItem.spec.tsx | 138 +++++- webview-ui/src/i18n/locales/en/settings.json | 2 + 10 files changed, 845 insertions(+), 33 deletions(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 6aa478cccba..21bc59092a1 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -604,6 +604,7 @@ export interface WebviewMessage { | "requestSkills" | "createSkill" | "deleteSkill" + | "moveSkill" | "openSkillFile" text?: string editedMessageContent?: string @@ -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[] diff --git a/src/core/webview/__tests__/skillsMessageHandler.spec.ts b/src/core/webview/__tests__/skillsMessageHandler.spec.ts index 900796dd6c3..f26194ee812 100644 --- a/src/core/webview/__tests__/skillsMessageHandler.spec.ts +++ b/src/core/webview/__tests__/skillsMessageHandler.spec.ts @@ -26,7 +26,9 @@ 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 }, @@ -34,7 +36,13 @@ vi.mock("../../../i18n", () => ({ 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() @@ -42,6 +50,7 @@ describe("skillsMessageHandler", () => { 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 => { @@ -50,6 +59,7 @@ describe("skillsMessageHandler", () => { getSkillsMetadata: mockGetSkillsMetadata, createSkill: mockCreateSkill, deleteSkill: mockDeleteSkill, + moveSkill: mockMoveSkill, getSkill: mockGetSkill, } : undefined @@ -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) diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index ae284a8d551..f09f22f58c5 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -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 { + 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 */ diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 051586b119d..73ca3c60bf6 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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" @@ -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 diff --git a/src/i18n/locales/en/skills.json b/src/i18n/locales/en/skills.json index 3a86dc3293d..ef4d7e68e3d 100644 --- a/src/i18n/locales/en/skills.json +++ b/src/i18n/locales/en/skills.json @@ -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" } } diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts index 6f7eeadc736..85c81f918bb 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -400,6 +400,78 @@ Add your skill instructions here. await this.discoverSkills() } + /** + * Move a skill to a different mode + * @param name - Skill name to move + * @param source - Where the skill is located ("global" or "project") + * @param currentMode - Current mode (undefined for generic skills) + * @param newMode - Target mode (undefined for generic skills) + */ + async moveSkill( + name: string, + source: "global" | "project", + currentMode: string | undefined, + newMode: string | undefined, + ): Promise { + // Don't move if source and destination are the same + if (currentMode === newMode) { + return + } + + // Find the skill at its current location + const skill = this.getSkill(name, source, currentMode) + if (!skill) { + const modeInfo = currentMode ? ` (mode: ${currentMode})` : "" + throw new Error(t("skills:errors.not_found", { name, source, modeInfo })) + } + + // Determine base directory + let baseDir: string + if (source === "global") { + baseDir = getGlobalRooDirectory() + } else { + const provider = this.providerRef.deref() + if (!provider?.cwd) { + throw new Error(t("skills:errors.no_workspace")) + } + baseDir = path.join(provider.cwd, ".roo") + } + + // Determine source and destination directories + const sourceDirName = currentMode ? `skills-${currentMode}` : "skills" + const destDirName = newMode ? `skills-${newMode}` : "skills" + const sourceDir = path.join(baseDir, sourceDirName, name) + const destSkillsDir = path.join(baseDir, destDirName) + const destDir = path.join(destSkillsDir, name) + const destSkillMdPath = path.join(destDir, "SKILL.md") + + // Check if skill already exists at destination + if (await fileExists(destSkillMdPath)) { + const newModeInfo = newMode ? ` (mode: ${newMode})` : "" + throw new Error(t("skills:errors.already_exists", { name, path: destSkillMdPath })) + } + + // Ensure destination skills directory exists + await fs.mkdir(destSkillsDir, { recursive: true }) + + // Move the skill directory + await fs.rename(sourceDir, destDir) + + // Clean up empty source skills directory + const sourceSkillsDir = path.join(baseDir, sourceDirName) + try { + const entries = await fs.readdir(sourceSkillsDir) + if (entries.length === 0) { + await fs.rmdir(sourceSkillsDir) + } + } catch { + // Ignore errors - directory might not exist or have permission issues + } + + // Refresh skills list + await this.discoverSkills() + } + /** * Get all skills directories to scan, including mode-specific directories. */ diff --git a/src/services/skills/__tests__/SkillsManager.spec.ts b/src/services/skills/__tests__/SkillsManager.spec.ts index 407c8353c13..9c02769ce8d 100644 --- a/src/services/skills/__tests__/SkillsManager.spec.ts +++ b/src/services/skills/__tests__/SkillsManager.spec.ts @@ -12,6 +12,8 @@ const { mockMkdir, mockWriteFile, mockRm, + mockRename, + mockRmdir, } = vi.hoisted(() => ({ mockStat: vi.fn(), mockReadFile: vi.fn(), @@ -23,6 +25,8 @@ const { mockMkdir: vi.fn(), mockWriteFile: vi.fn(), mockRm: vi.fn(), + mockRename: vi.fn(), + mockRmdir: vi.fn(), })) // Platform-agnostic test paths @@ -44,6 +48,8 @@ vi.mock("fs/promises", () => ({ mkdir: mockMkdir, writeFile: mockWriteFile, rm: mockRm, + rename: mockRename, + rmdir: mockRmdir, }, stat: mockStat, readFile: mockReadFile, @@ -52,6 +58,8 @@ vi.mock("fs/promises", () => ({ mkdir: mockMkdir, writeFile: mockWriteFile, rm: mockRm, + rename: mockRename, + rmdir: mockRmdir, })) // Mock os module @@ -1134,4 +1142,402 @@ Instructions`) await expect(skillsManager.deleteSkill("non-existent", "global")).rejects.toThrow("not found") }) }) + + describe("moveSkill", () => { + it("should move a skill from generic to mode-specific directory", async () => { + const sourceDir = p(globalSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(GLOBAL_ROO_DIR, "skills-code", "test-skill") + const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-code") + + // Setup: skill exists in generic skills directory + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === globalSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === globalSkillsDir) { + return ["test-skill"] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in source + if (file === testSkillMd) return true + // Skill does not exist in destination + if (file === p(destDir, "SKILL.md")) return false + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockMkdir.mockResolvedValue(undefined) + mockRename.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Verify skill exists + expect(skillsManager.getSkill("test-skill", "global")).toBeDefined() + + // Move the skill to code mode + await skillsManager.moveSkill("test-skill", "global", undefined, "code") + + expect(mockMkdir).toHaveBeenCalledWith(destSkillsDir, { recursive: true }) + expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir) + }) + + it("should move a skill from one mode to another", async () => { + const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code") + const sourceDir = p(sourceSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill") + const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect") + + // Setup: skill exists in code mode directory + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === sourceSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === sourceSkillsDir) { + return ["test-skill"] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in source + if (file === testSkillMd) return true + // Skill does not exist in destination + if (file === p(destDir, "SKILL.md")) return false + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockMkdir.mockResolvedValue(undefined) + mockRename.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Verify skill exists with mode + expect(skillsManager.getSkill("test-skill", "global", "code")).toBeDefined() + + // Move the skill to architect mode + await skillsManager.moveSkill("test-skill", "global", "code", "architect") + + expect(mockMkdir).toHaveBeenCalledWith(destSkillsDir, { recursive: true }) + expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir) + }) + + it("should move a skill from mode-specific to generic directory", async () => { + const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code") + const sourceDir = p(sourceSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(globalSkillsDir, "test-skill") + + // Setup: skill exists in code mode directory + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === sourceSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === sourceSkillsDir) { + return ["test-skill"] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in source + if (file === testSkillMd) return true + // Skill does not exist in destination + if (file === p(destDir, "SKILL.md")) return false + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockMkdir.mockResolvedValue(undefined) + mockRename.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Verify skill exists with mode + expect(skillsManager.getSkill("test-skill", "global", "code")).toBeDefined() + + // Move the skill to generic (no mode) + await skillsManager.moveSkill("test-skill", "global", "code", undefined) + + expect(mockMkdir).toHaveBeenCalledWith(globalSkillsDir, { recursive: true }) + expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir) + }) + + it("should not do anything when source and destination modes are the same", async () => { + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === globalSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === globalSkillsDir) { + return ["test-skill"] + } + return [] + }) + + const testSkillDir = p(globalSkillsDir, "test-skill") + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === testSkillDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + return file === p(testSkillDir, "SKILL.md") + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + await skillsManager.discoverSkills() + + // Try to move skill to the same mode (undefined -> undefined) + await skillsManager.moveSkill("test-skill", "global", undefined, undefined) + + // Should not call rename + expect(mockRename).not.toHaveBeenCalled() + }) + + it("should throw error if skill does not exist", async () => { + mockDirectoryExists.mockResolvedValue(false) + mockRealpath.mockImplementation(async (p: string) => p) + mockReaddir.mockResolvedValue([]) + + await skillsManager.discoverSkills() + + await expect(skillsManager.moveSkill("non-existent", "global", undefined, "code")).rejects.toThrow( + "not found", + ) + }) + + it("should throw error if skill already exists at destination", async () => { + const sourceDir = p(globalSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(GLOBAL_ROO_DIR, "skills-code", "test-skill") + const destSkillMd = p(destDir, "SKILL.md") + + // Setup: skill exists in both locations + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === globalSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === globalSkillsDir) { + return ["test-skill"] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in both source and destination + if (file === testSkillMd) return true + if (file === destSkillMd) return true + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + await skillsManager.discoverSkills() + + await expect(skillsManager.moveSkill("test-skill", "global", undefined, "code")).rejects.toThrow( + "already exists", + ) + }) + + it("should clean up empty source skills directory after moving", async () => { + const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code") + const sourceDir = p(sourceSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill") + const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect") + + // Setup: skill exists in code mode directory + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === sourceSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + // Track readdir calls - return skill for discovery, empty for cleanup check + let readdirCallCount = 0 + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === sourceSkillsDir) { + readdirCallCount++ + // First call is for discovery, return the skill + // Second call is for cleanup check after move, return empty + if (readdirCallCount === 1) { + return ["test-skill"] + } + return [] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in source + if (file === testSkillMd) return true + // Skill does not exist in destination + if (file === p(destDir, "SKILL.md")) return false + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockMkdir.mockResolvedValue(undefined) + mockRename.mockResolvedValue(undefined) + mockRmdir.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Move the skill to architect mode + await skillsManager.moveSkill("test-skill", "global", "code", "architect") + + // Verify empty directory was cleaned up + expect(mockRmdir).toHaveBeenCalledWith(sourceSkillsDir) + }) + + it("should not clean up source skills directory if it still has other skills", async () => { + const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code") + const sourceDir = p(sourceSkillsDir, "test-skill") + const testSkillMd = p(sourceDir, "SKILL.md") + const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill") + const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect") + + // Setup: skill exists in code mode directory along with another skill + mockDirectoryExists.mockImplementation(async (dir: string) => { + return dir === sourceSkillsDir + }) + + mockRealpath.mockImplementation(async (pathArg: string) => pathArg) + + // Track readdir calls - return skill for discovery, non-empty for cleanup check + let readdirCallCount = 0 + mockReaddir.mockImplementation(async (dir: string) => { + if (dir === sourceSkillsDir) { + readdirCallCount++ + // First call is for discovery + if (readdirCallCount === 1) { + return ["test-skill", "another-skill"] + } + // Second call for cleanup - still has another skill + return ["another-skill"] + } + return [] + }) + + mockStat.mockImplementation(async (pathArg: string) => { + if (pathArg === sourceDir || pathArg === p(sourceSkillsDir, "another-skill")) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + // Skill exists in source + if (file === testSkillMd) return true + if (file === p(sourceSkillsDir, "another-skill", "SKILL.md")) return true + // Skill does not exist in destination + if (file === p(destDir, "SKILL.md")) return false + return false + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockMkdir.mockResolvedValue(undefined) + mockRename.mockResolvedValue(undefined) + mockRmdir.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Move the skill to architect mode + await skillsManager.moveSkill("test-skill", "global", "code", "architect") + + // Verify directory was NOT cleaned up (still has other skills) + expect(mockRmdir).not.toHaveBeenCalled() + }) + }) }) diff --git a/webview-ui/src/components/settings/SkillItem.tsx b/webview-ui/src/components/settings/SkillItem.tsx index c1539d21b89..cd11f4553d4 100644 --- a/webview-ui/src/components/settings/SkillItem.tsx +++ b/webview-ui/src/components/settings/SkillItem.tsx @@ -1,10 +1,17 @@ -import React from "react" +import React, { useCallback, useMemo } from "react" import { Edit, Trash2 } from "lucide-react" import type { SkillMetadata } from "@roo-code/types" +import { getAllModes } from "@roo/modes" + import { useAppTranslation } from "@/i18n/TranslationContext" -import { Button, StandardTooltip } from "@/components/ui" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { Button, StandardTooltip, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui" +import { vscode } from "@/utils/vscode" + +// Sentinel value for "Any mode" since Radix Select doesn't allow empty string values +const MODE_ANY = "__any__" interface SkillItemProps { skill: SkillMetadata @@ -14,6 +21,40 @@ interface SkillItemProps { export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) => { const { t } = useAppTranslation() + const { customModes } = useExtensionState() + + // Get available modes for the dropdown (built-in + custom modes) + const availableModes = useMemo(() => { + return getAllModes(customModes).map((m) => ({ slug: m.slug, name: m.name })) + }, [customModes]) + + // Current mode value for the select (using sentinel for "Any mode") + const currentModeValue = skill.mode || MODE_ANY + + // Handle mode change + const handleModeChange = useCallback( + (newModeValue: string) => { + const newMode = newModeValue === MODE_ANY ? undefined : newModeValue + + // Don't do anything if mode hasn't changed + if (newMode === skill.mode) { + return + } + + // Send message to move skill to new mode + vscode.postMessage({ + type: "moveSkill", + skillName: skill.name, + source: skill.source, + skillMode: skill.mode, + newSkillMode: newMode, + }) + }, + [skill.name, skill.source, skill.mode], + ) + + // Built-in skills cannot change mode + const isBuiltIn = skill.source === "built-in" return (
@@ -21,17 +62,37 @@ export const SkillItem: React.FC = ({ skill, onEdit, onDelete })
{skill.name} - {skill.mode && ( - - {skill.mode} - - )}
{skill.description && (
{skill.description}
)}
+ {/* Mode dropdown */} +
+ {isBuiltIn ? ( + + {skill.mode || t("settings:skills.modeAny")} + + ) : ( + + + + )} +
+ {/* Action buttons */}
@@ -45,16 +106,18 @@ export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) - - - + {!isBuiltIn && ( + + + + )}
) diff --git a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx index 4c7eb6596c5..a8406e62944 100644 --- a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx @@ -3,6 +3,7 @@ import { render, screen, fireEvent } from "@/utils/test-utils" import type { SkillMetadata } from "@roo-code/types" import { SkillItem } from "../SkillItem" +import { vscode } from "@/utils/vscode" // Mock vscode vi.mock("@/utils/vscode", () => ({ @@ -14,11 +15,35 @@ vi.mock("@/utils/vscode", () => ({ // Mock the translation hook vi.mock("@/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => { + const translations: Record = { + "settings:skills.editSkill": "Edit skill", + "settings:skills.deleteSkill": "Delete skill", + "settings:skills.changeMode": "Change mode", + "settings:skills.modeAny": "Any mode", + } + return translations[key] || key + }, }), })) -// Mock UI components +// Mock getAllModes +vi.mock("@roo/modes", () => ({ + getAllModes: () => [ + { slug: "code", name: "💻 Code" }, + { slug: "architect", name: "🏗️ Architect" }, + { slug: "ask", name: "❓ Ask" }, + ], +})) + +// Mock useExtensionState +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + customModes: [], + }), +})) + +// Mock UI components - need to support Select components vi.mock("@/components/ui", () => ({ Button: ({ children, onClick, className, tabIndex }: any) => ( + + ), + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) => ( +
+ {children} +
+ ), + SelectTrigger: ({ children, className }: any) => ( + + ), + SelectValue: () => Value, })) const mockSkill: SkillMetadata = { @@ -47,6 +92,13 @@ const mockSkillWithMode: SkillMetadata = { mode: "architect", } +const mockBuiltInSkill: SkillMetadata = { + name: "built-in-skill", + description: "A built-in skill", + path: "", + source: "built-in", +} + describe("SkillItem", () => { const mockOnEdit = vi.fn() const mockOnDelete = vi.fn() @@ -67,18 +119,33 @@ describe("SkillItem", () => { expect(screen.getByText("A test skill description")).toBeInTheDocument() }) - it("renders mode badge when skill has mode", () => { + it("renders mode dropdown for non-built-in skills", () => { + render() + + expect(screen.getByTestId("select")).toBeInTheDocument() + }) + + it("renders mode dropdown with correct current value", () => { render() - expect(screen.getByText("architect")).toBeInTheDocument() + const select = screen.getByTestId("select") + expect(select).toHaveAttribute("data-value", "architect") }) - it("does not render mode badge when skill has no mode", () => { + it("renders mode dropdown with __any__ for skills without mode", () => { render() - // Should not have any mode badge - const container = screen.getByText("test-skill").parentElement - expect(container?.querySelector(".bg-vscode-badge-background")).toBeNull() + const select = screen.getByTestId("select") + expect(select).toHaveAttribute("data-value", "__any__") + }) + + it("does not render mode dropdown for built-in skills", () => { + render() + + // Should not have a select element + expect(screen.queryByTestId("select")).not.toBeInTheDocument() + // Should have a static badge instead + expect(screen.getByText("Any mode")).toBeInTheDocument() }) it("calls onEdit when edit button is clicked", () => { @@ -91,16 +158,24 @@ describe("SkillItem", () => { expect(mockOnEdit).toHaveBeenCalledTimes(1) }) - it("calls onDelete when delete button is clicked", () => { + it("calls onDelete when delete button is clicked for non-built-in skills", () => { render() const buttons = screen.getAllByTestId("button") - // Second button is delete + // Find the delete button (second one for non-built-in) fireEvent.click(buttons[1]) expect(mockOnDelete).toHaveBeenCalledTimes(1) }) + it("does not render delete button for built-in skills", () => { + render() + + // Should only have 1 button (edit) for built-in skills + const buttons = screen.getAllByTestId("button") + expect(buttons).toHaveLength(1) + }) + it("calls onEdit when clicking on skill name area", () => { render() @@ -110,6 +185,38 @@ describe("SkillItem", () => { expect(mockOnEdit).toHaveBeenCalledTimes(1) }) + it("sends moveSkill message when mode is changed", () => { + render() + + // Simulate mode change + const changeButton = screen.getByTestId("select-change-button") + fireEvent.click(changeButton) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "moveSkill", + skillName: "test-skill", + source: "project", + skillMode: undefined, + newSkillMode: "code", + }) + }) + + it("sends moveSkill message with correct current mode", () => { + render() + + // Simulate mode change + const changeButton = screen.getByTestId("select-change-button") + fireEvent.click(changeButton) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "moveSkill", + skillName: "mode-specific-skill", + source: "global", + skillMode: "architect", + newSkillMode: "code", + }) + }) + it("renders without description when not provided", () => { const skillWithoutDescription: SkillMetadata = { name: "no-desc-skill", @@ -132,10 +239,19 @@ describe("SkillItem", () => { expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground") }) - it("renders both edit and delete buttons", () => { + it("renders edit and delete buttons for non-built-in skills", () => { render() const buttons = screen.getAllByTestId("button") expect(buttons).toHaveLength(2) }) + + it("includes available modes in the dropdown", () => { + render() + + // Check that select items are rendered + const selectItems = screen.getAllByTestId("select-item") + // Should have "Any mode" + 3 modes (code, architect, ask) + expect(selectItems).toHaveLength(4) + }) }) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3230c4f93f8..de76ba4c679 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -80,6 +80,8 @@ "addSkill": "Add Skill", "editSkill": "Edit skill", "deleteSkill": "Delete skill", + "changeMode": "Change mode", + "modeAny": "Any mode", "deleteDialog": { "title": "Delete Skill", "description": "Are you sure you want to delete the skill \"{{name}}\"? This action cannot be undone.", From 23a7f55f5bbbcbcffcd7a5b523847b894b6af5e8 Mon Sep 17 00:00:00 2001 From: Sannidhya Date: Fri, 30 Jan 2026 09:18:55 +0530 Subject: [PATCH 2/3] feat(i18n): add translations for skills mode dropdown feature - Add missing_move_fields error translation for all 17 locales - Add changeMode and modeAny UI translations for all 17 locales - Covers: ca, de, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW --- src/i18n/locales/ca/skills.json | 1 + src/i18n/locales/de/skills.json | 1 + src/i18n/locales/es/skills.json | 1 + src/i18n/locales/fr/skills.json | 1 + src/i18n/locales/hi/skills.json | 1 + src/i18n/locales/id/skills.json | 1 + src/i18n/locales/it/skills.json | 1 + src/i18n/locales/ja/skills.json | 1 + src/i18n/locales/ko/skills.json | 1 + src/i18n/locales/nl/skills.json | 1 + src/i18n/locales/pl/skills.json | 1 + src/i18n/locales/pt-BR/skills.json | 1 + src/i18n/locales/ru/skills.json | 1 + src/i18n/locales/tr/skills.json | 1 + src/i18n/locales/vi/skills.json | 1 + src/i18n/locales/zh-CN/skills.json | 1 + src/i18n/locales/zh-TW/skills.json | 1 + webview-ui/src/i18n/locales/ca/settings.json | 2 ++ webview-ui/src/i18n/locales/de/settings.json | 2 ++ webview-ui/src/i18n/locales/es/settings.json | 2 ++ webview-ui/src/i18n/locales/fr/settings.json | 2 ++ webview-ui/src/i18n/locales/hi/settings.json | 2 ++ webview-ui/src/i18n/locales/id/settings.json | 2 ++ webview-ui/src/i18n/locales/it/settings.json | 2 ++ webview-ui/src/i18n/locales/ja/settings.json | 2 ++ webview-ui/src/i18n/locales/ko/settings.json | 2 ++ webview-ui/src/i18n/locales/nl/settings.json | 2 ++ webview-ui/src/i18n/locales/pl/settings.json | 2 ++ webview-ui/src/i18n/locales/pt-BR/settings.json | 2 ++ webview-ui/src/i18n/locales/ru/settings.json | 2 ++ webview-ui/src/i18n/locales/tr/settings.json | 2 ++ webview-ui/src/i18n/locales/vi/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-CN/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-TW/settings.json | 2 ++ 34 files changed, 51 insertions(+) diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json index 1879c78a49b..74d8cba0393 100644 --- a/src/i18n/locales/ca/skills.json +++ b/src/i18n/locales/ca/skills.json @@ -7,6 +7,7 @@ "already_exists": "L'habilitat \"{{name}}\" ja existeix a {{path}}", "not_found": "No s'ha trobat l'habilitat \"{{name}}\" a {{source}}{{modeInfo}}", "missing_create_fields": "Falten camps obligatoris: skillName, source o skillDescription", + "missing_move_fields": "Falten camps obligatoris: skillName o source", "manager_unavailable": "El gestor d'habilitats no està disponible", "missing_delete_fields": "Falten camps obligatoris: skillName o source", "skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"", diff --git a/src/i18n/locales/de/skills.json b/src/i18n/locales/de/skills.json index 614cc9ed633..5aad37950f7 100644 --- a/src/i18n/locales/de/skills.json +++ b/src/i18n/locales/de/skills.json @@ -7,6 +7,7 @@ "already_exists": "Skill \"{{name}}\" existiert bereits unter {{path}}", "not_found": "Skill \"{{name}}\" nicht gefunden in {{source}}{{modeInfo}}", "missing_create_fields": "Erforderliche Felder fehlen: skillName, source oder skillDescription", + "missing_move_fields": "Erforderliche Felder fehlen: skillName oder source", "manager_unavailable": "Skill-Manager nicht verfügbar", "missing_delete_fields": "Erforderliche Felder fehlen: skillName oder source", "skill_not_found": "Skill \"{{name}}\" nicht gefunden", diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json index c6e8aacebed..65345815182 100644 --- a/src/i18n/locales/es/skills.json +++ b/src/i18n/locales/es/skills.json @@ -7,6 +7,7 @@ "already_exists": "La habilidad \"{{name}}\" ya existe en {{path}}", "not_found": "No se encontró la habilidad \"{{name}}\" en {{source}}{{modeInfo}}", "missing_create_fields": "Faltan campos obligatorios: skillName, source o skillDescription", + "missing_move_fields": "Faltan campos obligatorios: skillName o source", "manager_unavailable": "El gestor de habilidades no está disponible", "missing_delete_fields": "Faltan campos obligatorios: skillName o source", "skill_not_found": "No se encontró la habilidad \"{{name}}\"", diff --git a/src/i18n/locales/fr/skills.json b/src/i18n/locales/fr/skills.json index 6e1cc8e9d0c..5c4cb1f5ae0 100644 --- a/src/i18n/locales/fr/skills.json +++ b/src/i18n/locales/fr/skills.json @@ -7,6 +7,7 @@ "already_exists": "La compétence \"{{name}}\" existe déjà à {{path}}", "not_found": "Compétence \"{{name}}\" introuvable dans {{source}}{{modeInfo}}", "missing_create_fields": "Champs obligatoires manquants : skillName, source ou skillDescription", + "missing_move_fields": "Champs obligatoires manquants : skillName ou source", "manager_unavailable": "Le gestionnaire de compétences n'est pas disponible", "missing_delete_fields": "Champs obligatoires manquants : skillName ou source", "skill_not_found": "Compétence \"{{name}}\" introuvable", diff --git a/src/i18n/locales/hi/skills.json b/src/i18n/locales/hi/skills.json index 63191af5d05..50929b48459 100644 --- a/src/i18n/locales/hi/skills.json +++ b/src/i18n/locales/hi/skills.json @@ -7,6 +7,7 @@ "already_exists": "स्किल \"{{name}}\" पहले से {{path}} पर मौजूद है", "not_found": "स्किल \"{{name}}\" {{source}}{{modeInfo}} में नहीं मिला", "missing_create_fields": "आवश्यक फ़ील्ड गायब हैं: skillName, source, या skillDescription", + "missing_move_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", "manager_unavailable": "स्किल मैनेजर उपलब्ध नहीं है", "missing_delete_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", "skill_not_found": "स्किल \"{{name}}\" नहीं मिला", diff --git a/src/i18n/locales/id/skills.json b/src/i18n/locales/id/skills.json index 379421b39c6..cfa01b33234 100644 --- a/src/i18n/locales/id/skills.json +++ b/src/i18n/locales/id/skills.json @@ -7,6 +7,7 @@ "already_exists": "Skill \"{{name}}\" sudah ada di {{path}}", "not_found": "Skill \"{{name}}\" tidak ditemukan di {{source}}{{modeInfo}}", "missing_create_fields": "Bidang wajib tidak ada: skillName, source, atau skillDescription", + "missing_move_fields": "Bidang wajib tidak ada: skillName atau source", "manager_unavailable": "Manajer skill tidak tersedia", "missing_delete_fields": "Bidang wajib tidak ada: skillName atau source", "skill_not_found": "Skill \"{{name}}\" tidak ditemukan", diff --git a/src/i18n/locales/it/skills.json b/src/i18n/locales/it/skills.json index 4e7ac0495bb..0ddcf0f70c0 100644 --- a/src/i18n/locales/it/skills.json +++ b/src/i18n/locales/it/skills.json @@ -7,6 +7,7 @@ "already_exists": "La skill \"{{name}}\" esiste già in {{path}}", "not_found": "Skill \"{{name}}\" non trovata in {{source}}{{modeInfo}}", "missing_create_fields": "Campi obbligatori mancanti: skillName, source o skillDescription", + "missing_move_fields": "Campi obbligatori mancanti: skillName o source", "manager_unavailable": "Il gestore delle skill non è disponibile", "missing_delete_fields": "Campi obbligatori mancanti: skillName o source", "skill_not_found": "Skill \"{{name}}\" non trovata", diff --git a/src/i18n/locales/ja/skills.json b/src/i18n/locales/ja/skills.json index da8f3f8566b..16576b2be35 100644 --- a/src/i18n/locales/ja/skills.json +++ b/src/i18n/locales/ja/skills.json @@ -7,6 +7,7 @@ "already_exists": "スキル「{{name}}」は既に{{path}}に存在します", "not_found": "スキル「{{name}}」が{{source}}{{modeInfo}}に見つかりません", "missing_create_fields": "必須フィールドが不足しています:skillName、source、またはskillDescription", + "missing_move_fields": "必須フィールドが不足しています:skillNameまたはsource", "manager_unavailable": "スキルマネージャーが利用できません", "missing_delete_fields": "必須フィールドが不足しています:skillNameまたはsource", "skill_not_found": "スキル「{{name}}」が見つかりません", diff --git a/src/i18n/locales/ko/skills.json b/src/i18n/locales/ko/skills.json index 040fcd2950b..c5808f3630b 100644 --- a/src/i18n/locales/ko/skills.json +++ b/src/i18n/locales/ko/skills.json @@ -7,6 +7,7 @@ "already_exists": "스킬 \"{{name}}\"이(가) 이미 {{path}}에 존재합니다", "not_found": "{{source}}{{modeInfo}}에서 스킬 \"{{name}}\"을(를) 찾을 수 없습니다", "missing_create_fields": "필수 필드 누락: skillName, source 또는 skillDescription", + "missing_move_fields": "필수 필드 누락: skillName 또는 source", "manager_unavailable": "스킬 관리자를 사용할 수 없습니다", "missing_delete_fields": "필수 필드 누락: skillName 또는 source", "skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다", diff --git a/src/i18n/locales/nl/skills.json b/src/i18n/locales/nl/skills.json index 17375070901..6c6e7e0e832 100644 --- a/src/i18n/locales/nl/skills.json +++ b/src/i18n/locales/nl/skills.json @@ -7,6 +7,7 @@ "already_exists": "Vaardigheid \"{{name}}\" bestaat al op {{path}}", "not_found": "Vaardigheid \"{{name}}\" niet gevonden in {{source}}{{modeInfo}}", "missing_create_fields": "Vereiste velden ontbreken: skillName, source of skillDescription", + "missing_move_fields": "Vereiste velden ontbreken: skillName of source", "manager_unavailable": "Vaardigheidenbeheerder niet beschikbaar", "missing_delete_fields": "Vereiste velden ontbreken: skillName of source", "skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden", diff --git a/src/i18n/locales/pl/skills.json b/src/i18n/locales/pl/skills.json index dbbb883d013..f9363e42d03 100644 --- a/src/i18n/locales/pl/skills.json +++ b/src/i18n/locales/pl/skills.json @@ -7,6 +7,7 @@ "already_exists": "Umiejętność \"{{name}}\" już istnieje w {{path}}", "not_found": "Nie znaleziono umiejętności \"{{name}}\" w {{source}}{{modeInfo}}", "missing_create_fields": "Brakuje wymaganych pól: skillName, source lub skillDescription", + "missing_move_fields": "Brakuje wymaganych pól: skillName lub source", "manager_unavailable": "Menedżer umiejętności niedostępny", "missing_delete_fields": "Brakuje wymaganych pól: skillName lub source", "skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"", diff --git a/src/i18n/locales/pt-BR/skills.json b/src/i18n/locales/pt-BR/skills.json index 9ed29abfdec..8058e9f6a38 100644 --- a/src/i18n/locales/pt-BR/skills.json +++ b/src/i18n/locales/pt-BR/skills.json @@ -7,6 +7,7 @@ "already_exists": "A habilidade \"{{name}}\" já existe em {{path}}", "not_found": "Habilidade \"{{name}}\" não encontrada em {{source}}{{modeInfo}}", "missing_create_fields": "Campos obrigatórios ausentes: skillName, source ou skillDescription", + "missing_move_fields": "Campos obrigatórios ausentes: skillName ou source", "manager_unavailable": "Gerenciador de habilidades não disponível", "missing_delete_fields": "Campos obrigatórios ausentes: skillName ou source", "skill_not_found": "Habilidade \"{{name}}\" não encontrada", diff --git a/src/i18n/locales/ru/skills.json b/src/i18n/locales/ru/skills.json index 2429e9a17ac..627a8fd4d65 100644 --- a/src/i18n/locales/ru/skills.json +++ b/src/i18n/locales/ru/skills.json @@ -7,6 +7,7 @@ "already_exists": "Навык \"{{name}}\" уже существует в {{path}}", "not_found": "Навык \"{{name}}\" не найден в {{source}}{{modeInfo}}", "missing_create_fields": "Отсутствуют обязательные поля: skillName, source или skillDescription", + "missing_move_fields": "Отсутствуют обязательные поля: skillName или source", "manager_unavailable": "Менеджер навыков недоступен", "missing_delete_fields": "Отсутствуют обязательные поля: skillName или source", "skill_not_found": "Навык \"{{name}}\" не найден", diff --git a/src/i18n/locales/tr/skills.json b/src/i18n/locales/tr/skills.json index eadab29a13f..e7781aa696b 100644 --- a/src/i18n/locales/tr/skills.json +++ b/src/i18n/locales/tr/skills.json @@ -7,6 +7,7 @@ "already_exists": "\"{{name}}\" becerisi zaten {{path}} konumunda mevcut", "not_found": "\"{{name}}\" becerisi {{source}}{{modeInfo}} içinde bulunamadı", "missing_create_fields": "Gerekli alanlar eksik: skillName, source veya skillDescription", + "missing_move_fields": "Gerekli alanlar eksik: skillName veya source", "manager_unavailable": "Beceri yöneticisi kullanılamıyor", "missing_delete_fields": "Gerekli alanlar eksik: skillName veya source", "skill_not_found": "\"{{name}}\" becerisi bulunamadı", diff --git a/src/i18n/locales/vi/skills.json b/src/i18n/locales/vi/skills.json index b36131b6bf6..f97b7ed2b07 100644 --- a/src/i18n/locales/vi/skills.json +++ b/src/i18n/locales/vi/skills.json @@ -7,6 +7,7 @@ "already_exists": "Kỹ năng \"{{name}}\" đã tồn tại tại {{path}}", "not_found": "Không tìm thấy kỹ năng \"{{name}}\" trong {{source}}{{modeInfo}}", "missing_create_fields": "Thiếu các trường bắt buộc: skillName, source hoặc skillDescription", + "missing_move_fields": "Thiếu các trường bắt buộc: skillName hoặc source", "manager_unavailable": "Trình quản lý kỹ năng không khả dụng", "missing_delete_fields": "Thiếu các trường bắt buộc: skillName hoặc source", "skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"", diff --git a/src/i18n/locales/zh-CN/skills.json b/src/i18n/locales/zh-CN/skills.json index 46885cb0d4c..566f583feee 100644 --- a/src/i18n/locales/zh-CN/skills.json +++ b/src/i18n/locales/zh-CN/skills.json @@ -7,6 +7,7 @@ "already_exists": "技能 \"{{name}}\" 已存在于 {{path}}", "not_found": "在 {{source}}{{modeInfo}} 中未找到技能 \"{{name}}\"", "missing_create_fields": "缺少必填字段:skillName、source 或 skillDescription", + "missing_move_fields": "缺少必填字段:skillName 或 source", "manager_unavailable": "技能管理器不可用", "missing_delete_fields": "缺少必填字段:skillName 或 source", "skill_not_found": "未找到技能 \"{{name}}\"", diff --git a/src/i18n/locales/zh-TW/skills.json b/src/i18n/locales/zh-TW/skills.json index 0ff0969e578..633bb1a6b2b 100644 --- a/src/i18n/locales/zh-TW/skills.json +++ b/src/i18n/locales/zh-TW/skills.json @@ -7,6 +7,7 @@ "already_exists": "技能「{{name}}」已存在於 {{path}}", "not_found": "在 {{source}}{{modeInfo}} 中找不到技能「{{name}}」", "missing_create_fields": "缺少必填欄位:skillName、source 或 skillDescription", + "missing_move_fields": "缺少必填欄位:skillName 或 source", "manager_unavailable": "技能管理器無法使用", "missing_delete_fields": "缺少必填欄位:skillName 或 source", "skill_not_found": "找不到技能「{{name}}」", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 85e4abadcc5..7a4e0019c59 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1003,6 +1003,8 @@ "addSkill": "Afegir Skill", "editSkill": "Editar skill", "deleteSkill": "Eliminar skill", + "changeMode": "Canvia mode", + "modeAny": "Qualsevol mode", "deleteDialog": { "title": "Eliminar Skill", "description": "Estàs segur que vols eliminar la skill \"{{name}}\"? Aquesta acció no es pot desfer.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index ff7e7b3ea0c..d5182077809 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1003,6 +1003,8 @@ "addSkill": "Skill hinzufügen", "editSkill": "Skill bearbeiten", "deleteSkill": "Skill löschen", + "changeMode": "Modus ändern", + "modeAny": "Beliebiger Modus", "deleteDialog": { "title": "Skill löschen", "description": "Sind Sie sicher, dass Sie die Skill \"{{name}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index d02b5d05a76..3391076f616 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1003,6 +1003,8 @@ "addSkill": "Añadir Skill", "editSkill": "Editar skill", "deleteSkill": "Eliminar skill", + "changeMode": "Cambiar modo", + "modeAny": "Cualquier modo", "deleteDialog": { "title": "Eliminar Skill", "description": "¿Estás seguro de que quieres eliminar la skill \"{{name}}\"? Esta acción no se puede deshacer.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index d86c1652bc2..402d701d029 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1003,6 +1003,8 @@ "addSkill": "Ajouter une Skill", "editSkill": "Modifier la skill", "deleteSkill": "Supprimer la skill", + "changeMode": "Changer de mode", + "modeAny": "N'importe quel mode", "deleteDialog": { "title": "Supprimer la Skill", "description": "Êtes-vous sûr de vouloir supprimer la skill \"{{name}}\" ? Cette action ne peut pas être annulée.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 6200d228499..66289df2984 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Skill जोड़ें", "editSkill": "Skill संपादित करें", "deleteSkill": "Skill हटाएं", + "changeMode": "मोड बदलें", + "modeAny": "कोई भी मोड", "deleteDialog": { "title": "Skill हटाएं", "description": "क्या आप वाकई skill \"{{name}}\" को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 39e0eccb7f8..e166cda621b 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1033,6 +1033,8 @@ "addSkill": "Tambahkan Skill", "editSkill": "Edit skill", "deleteSkill": "Hapus skill", + "changeMode": "Ubah mode", + "modeAny": "Mode apa saja", "deleteDialog": { "title": "Hapus Skill", "description": "Apakah Anda yakin ingin menghapus skill \"{{name}}\"? Tindakan ini tidak dapat dibatalkan.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 74c3da5757d..351db0a9cc7 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Aggiungi Skill", "editSkill": "Modifica skill", "deleteSkill": "Elimina skill", + "changeMode": "Cambia modalità", + "modeAny": "Qualsiasi modalità", "deleteDialog": { "title": "Elimina Skill", "description": "Sei sicuro di voler eliminare la skill \"{{name}}\"? Questa azione non può essere annullata.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index ae98a2b01b1..79ecee17e36 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "スキルを追加", "editSkill": "スキルを編集", "deleteSkill": "スキルを削除", + "changeMode": "モードを変更", + "modeAny": "任意のモード", "deleteDialog": { "title": "スキルを削除", "description": "スキル「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index db6ba45dbc5..c044a22f22f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "스킬 추가", "editSkill": "스킬 편집", "deleteSkill": "스킬 삭제", + "changeMode": "모드 변경", + "modeAny": "모든 모드", "deleteDialog": { "title": "스킬 삭제", "description": "스킬 \"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 49d02217db2..f4cd21e66bb 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Skill toevoegen", "editSkill": "Skill bewerken", "deleteSkill": "Skill verwijderen", + "changeMode": "Modus wijzigen", + "modeAny": "Elke modus", "deleteDialog": { "title": "Skill verwijderen", "description": "Weet u zeker dat u de skill \"{{name}}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index a20c9f56e14..632f9811df1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Dodaj Umiejętność", "editSkill": "Edytuj umiejętność", "deleteSkill": "Usuń umiejętność", + "changeMode": "Zmień tryb", + "modeAny": "Dowolny tryb", "deleteDialog": { "title": "Usuń Umiejętność", "description": "Czy na pewno chcesz usunąć umiejętność \"{{name}}\"? Tej akcji nie można cofnąć.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index e96cbb0e743..f30ac10bdb3 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Adicionar Skill", "editSkill": "Editar skill", "deleteSkill": "Excluir skill", + "changeMode": "Alterar modo", + "modeAny": "Qualquer modo", "deleteDialog": { "title": "Excluir Skill", "description": "Tem certeza de que deseja excluir a skill \"{{name}}\"? Esta ação não pode ser desfeita.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a746f1e29ae..98b9d965652 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Добавить Навык", "editSkill": "Редактировать навык", "deleteSkill": "Удалить навык", + "changeMode": "Изменить режим", + "modeAny": "Любой режим", "deleteDialog": { "title": "Удалить Навык", "description": "Вы уверены, что хотите удалить навык \"{{name}}\"? Это действие нельзя отменить.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index cbbbb673cc2..872f2ea3c73 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Beceri Ekle", "editSkill": "Beceriyi düzenle", "deleteSkill": "Beceriyi sil", + "changeMode": "Modu değiştir", + "modeAny": "Herhangi bir mod", "deleteDialog": { "title": "Beceriyi Sil", "description": "\"{{name}}\" becerisini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index acbbea3688e..1eced935c26 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "Thêm Skill", "editSkill": "Chỉnh sửa skill", "deleteSkill": "Xóa skill", + "changeMode": "Thay đổi chế độ", + "modeAny": "Bất kỳ chế độ nào", "deleteDialog": { "title": "Xóa Skill", "description": "Bạn có chắc chắn muốn xóa skill \"{{name}}\" không? Hành động này không thể hoàn tác.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 57b1b7795c5..b70398c61ea 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1004,6 +1004,8 @@ "addSkill": "添加技能", "editSkill": "编辑技能", "deleteSkill": "删除技能", + "changeMode": "更改模式", + "modeAny": "任意模式", "deleteDialog": { "title": "删除技能", "description": "您确定要删除技能\"{{name}}\"吗?此操作无法撤销。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 04eb9970f23..506408ca73b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1011,6 +1011,8 @@ "addSkill": "新增技能", "editSkill": "編輯技能", "deleteSkill": "刪除技能", + "changeMode": "變更模式", + "modeAny": "任意模式", "deleteDialog": { "title": "刪除技能", "description": "您確定要刪除技能「{{name}}」嗎?此動作無法復原。", From f99f57f8d71b970feaa164dec4f1cf8b52a97b7b Mon Sep 17 00:00:00 2001 From: Sannidhya Date: Fri, 30 Jan 2026 09:22:54 +0530 Subject: [PATCH 3/3] fix: remove unused variable newModeInfo in moveSkill method --- src/services/skills/SkillsManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts index 85c81f918bb..05d879d9759 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -447,7 +447,6 @@ Add your skill instructions here. // Check if skill already exists at destination if (await fileExists(destSkillMdPath)) { - const newModeInfo = newMode ? ` (mode: ${newMode})` : "" throw new Error(t("skills:errors.already_exists", { name, path: destSkillMdPath })) }