From 2d87edd0497500cd7c5e1a6586b034e8b85c2acf Mon Sep 17 00:00:00 2001 From: SannidhyaSah Date: Sat, 31 Jan 2026 10:52:49 +0530 Subject: [PATCH 01/15] feat: add mode dropdown to change skill mode dynamically (#10513) (#11102) Co-authored-by: Sannidhya --- packages/types/src/vscode-extension-host.ts | 12 +- .../__tests__/skillsMessageHandler.spec.ts | 433 +++++++++++ src/core/webview/skillsMessageHandler.ts | 189 +++++ src/core/webview/webviewMessageHandler.ts | 27 + src/i18n/locales/ca/skills.json | 17 + src/i18n/locales/de/skills.json | 17 + src/i18n/locales/en/skills.json | 17 + src/i18n/locales/es/skills.json | 17 + src/i18n/locales/fr/skills.json | 17 + src/i18n/locales/hi/skills.json | 17 + src/i18n/locales/id/skills.json | 17 + src/i18n/locales/it/skills.json | 17 + src/i18n/locales/ja/skills.json | 17 + src/i18n/locales/ko/skills.json | 17 + src/i18n/locales/nl/skills.json | 17 + src/i18n/locales/pl/skills.json | 17 + src/i18n/locales/pt-BR/skills.json | 17 + src/i18n/locales/ru/skills.json | 17 + src/i18n/locales/tr/skills.json | 17 + src/i18n/locales/vi/skills.json | 17 + src/i18n/locales/zh-CN/skills.json | 17 + src/i18n/locales/zh-TW/skills.json | 17 + src/services/skills/SkillsManager.ts | 211 ++++++ .../skills/__tests__/SkillsManager.spec.ts | 709 +++++++++++++++++- .../src/components/settings/SkillItem.tsx | 124 +++ .../settings/__tests__/SkillItem.spec.tsx | 257 +++++++ webview-ui/src/i18n/locales/ca/settings.json | 56 +- webview-ui/src/i18n/locales/de/settings.json | 56 +- webview-ui/src/i18n/locales/en/settings.json | 54 +- webview-ui/src/i18n/locales/es/settings.json | 56 +- webview-ui/src/i18n/locales/fr/settings.json | 56 +- webview-ui/src/i18n/locales/hi/settings.json | 56 +- webview-ui/src/i18n/locales/id/settings.json | 56 +- webview-ui/src/i18n/locales/it/settings.json | 56 +- webview-ui/src/i18n/locales/ja/settings.json | 56 +- webview-ui/src/i18n/locales/ko/settings.json | 56 +- webview-ui/src/i18n/locales/nl/settings.json | 56 +- webview-ui/src/i18n/locales/pl/settings.json | 56 +- .../src/i18n/locales/pt-BR/settings.json | 56 +- webview-ui/src/i18n/locales/ru/settings.json | 56 +- webview-ui/src/i18n/locales/tr/settings.json | 56 +- webview-ui/src/i18n/locales/vi/settings.json | 56 +- .../src/i18n/locales/zh-CN/settings.json | 56 +- .../src/i18n/locales/zh-TW/settings.json | 66 +- 44 files changed, 3202 insertions(+), 82 deletions(-) create mode 100644 src/core/webview/__tests__/skillsMessageHandler.spec.ts create mode 100644 src/core/webview/skillsMessageHandler.ts create mode 100644 src/i18n/locales/ca/skills.json create mode 100644 src/i18n/locales/de/skills.json create mode 100644 src/i18n/locales/en/skills.json create mode 100644 src/i18n/locales/es/skills.json create mode 100644 src/i18n/locales/fr/skills.json create mode 100644 src/i18n/locales/hi/skills.json create mode 100644 src/i18n/locales/id/skills.json create mode 100644 src/i18n/locales/it/skills.json create mode 100644 src/i18n/locales/ja/skills.json create mode 100644 src/i18n/locales/ko/skills.json create mode 100644 src/i18n/locales/nl/skills.json create mode 100644 src/i18n/locales/pl/skills.json create mode 100644 src/i18n/locales/pt-BR/skills.json create mode 100644 src/i18n/locales/ru/skills.json create mode 100644 src/i18n/locales/tr/skills.json create mode 100644 src/i18n/locales/vi/skills.json create mode 100644 src/i18n/locales/zh-CN/skills.json create mode 100644 src/i18n/locales/zh-TW/skills.json create mode 100644 webview-ui/src/components/settings/SkillItem.tsx create mode 100644 webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index fcabae23882..0b7a28efc2d 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -608,6 +608,12 @@ export interface WebviewMessage { | "createWorktreeInclude" | "checkoutBranch" | "browseForWorktreePath" + // Skills messages + | "requestSkills" + | "createSkill" + | "deleteSkill" + | "moveSkill" + | "openSkillFile" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -641,7 +647,11 @@ export interface WebviewMessage { modeConfig?: ModeConfig timeout?: number payload?: WebViewMessagePayload - source?: "global" | "project" + source?: "global" | "project" | "built-in" + 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[] terminalOperation?: "continue" | "abort" diff --git a/src/core/webview/__tests__/skillsMessageHandler.spec.ts b/src/core/webview/__tests__/skillsMessageHandler.spec.ts new file mode 100644 index 00000000000..f26194ee812 --- /dev/null +++ b/src/core/webview/__tests__/skillsMessageHandler.spec.ts @@ -0,0 +1,433 @@ +// npx vitest run src/core/webview/__tests__/skillsMessageHandler.spec.ts + +import type { SkillMetadata, WebviewMessage } from "@roo-code/types" +import type { ClineProvider } from "../ClineProvider" + +// Mock vscode first +vi.mock("vscode", () => { + const showErrorMessage = vi.fn() + + return { + window: { + showErrorMessage, + }, + } +}) + +// Mock open-file +vi.mock("../../../integrations/misc/open-file", () => ({ + openFile: vi.fn(), +})) + +// Mock i18n +vi.mock("../../../i18n", () => ({ + t: (key: string, params?: Record) => { + const translations: Record = { + "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, + 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 => { + const skillsManager = hasSkillsManager + ? { + getSkillsMetadata: mockGetSkillsMetadata, + createSkill: mockCreateSkill, + deleteSkill: mockDeleteSkill, + moveSkill: mockMoveSkill, + getSkill: mockGetSkill, + } + : undefined + + return { + log: mockLog, + postMessageToWebview: mockPostMessageToWebview, + getSkillsManager: () => skillsManager, + } as unknown as ClineProvider + } + + const mockSkills: SkillMetadata[] = [ + { + name: "test-skill", + description: "Test skill description", + path: "/path/to/test-skill/SKILL.md", + source: "global", + }, + { + name: "project-skill", + description: "Project skill description", + path: "/project/.roo/skills/project-skill/SKILL.md", + source: "project", + mode: "code", + }, + ] + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("handleRequestSkills", () => { + it("returns skills when skills manager is available", async () => { + const provider = createMockProvider(true) + mockGetSkillsMetadata.mockReturnValue(mockSkills) + + const result = await handleRequestSkills(provider) + + expect(result).toEqual(mockSkills) + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: mockSkills }) + }) + + it("returns empty skills when skills manager is not available", async () => { + const provider = createMockProvider(false) + + const result = await handleRequestSkills(provider) + + expect(result).toEqual([]) + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [] }) + }) + + it("handles errors and returns empty skills", async () => { + const provider = createMockProvider(true) + mockGetSkillsMetadata.mockImplementation(() => { + throw new Error("Test error") + }) + + const result = await handleRequestSkills(provider) + + expect(result).toEqual([]) + expect(mockLog).toHaveBeenCalled() + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [] }) + }) + }) + + describe("handleCreateSkill", () => { + it("creates a skill successfully", async () => { + const provider = createMockProvider(true) + mockCreateSkill.mockResolvedValue("/path/to/new-skill/SKILL.md") + mockGetSkillsMetadata.mockReturnValue(mockSkills) + + const result = await handleCreateSkill(provider, { + type: "createSkill", + skillName: "new-skill", + source: "global", + skillDescription: "New skill description", + } as WebviewMessage) + + expect(result).toEqual(mockSkills) + expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "global", "New skill description", undefined) + expect(openFile).toHaveBeenCalledWith("/path/to/new-skill/SKILL.md") + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: mockSkills }) + }) + + it("creates a skill with mode restriction", async () => { + const provider = createMockProvider(true) + mockCreateSkill.mockResolvedValue("/path/to/new-skill/SKILL.md") + mockGetSkillsMetadata.mockReturnValue(mockSkills) + + const result = await handleCreateSkill(provider, { + type: "createSkill", + skillName: "new-skill", + source: "project", + skillDescription: "New skill description", + skillMode: "code", + } as WebviewMessage) + + expect(result).toEqual(mockSkills) + expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", "code") + }) + + it("returns undefined when required fields are missing", async () => { + const provider = createMockProvider(true) + + const result = await handleCreateSkill(provider, { + type: "createSkill", + skillName: "new-skill", + // missing source and skillDescription + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(mockLog).toHaveBeenCalledWith( + "Error creating skill: Missing required fields: skillName, source, or skillDescription", + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to create skill: Missing required fields: skillName, source, or skillDescription", + ) + }) + + it("returns undefined when skills manager is not available", async () => { + const provider = createMockProvider(false) + + const result = await handleCreateSkill(provider, { + type: "createSkill", + skillName: "new-skill", + source: "global", + skillDescription: "New skill description", + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(mockLog).toHaveBeenCalledWith("Error creating skill: Skills manager not available") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to create skill: Skills manager not available", + ) + }) + }) + + describe("handleDeleteSkill", () => { + it("deletes a skill successfully", async () => { + const provider = createMockProvider(true) + mockDeleteSkill.mockResolvedValue(undefined) + mockGetSkillsMetadata.mockReturnValue([mockSkills[1]]) + + const result = await handleDeleteSkill(provider, { + type: "deleteSkill", + skillName: "test-skill", + source: "global", + } as WebviewMessage) + + expect(result).toEqual([mockSkills[1]]) + expect(mockDeleteSkill).toHaveBeenCalledWith("test-skill", "global", undefined) + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [mockSkills[1]] }) + }) + + it("deletes a skill with mode restriction", async () => { + const provider = createMockProvider(true) + mockDeleteSkill.mockResolvedValue(undefined) + mockGetSkillsMetadata.mockReturnValue([mockSkills[0]]) + + const result = await handleDeleteSkill(provider, { + type: "deleteSkill", + skillName: "project-skill", + source: "project", + skillMode: "code", + } as WebviewMessage) + + expect(result).toEqual([mockSkills[0]]) + expect(mockDeleteSkill).toHaveBeenCalledWith("project-skill", "project", "code") + }) + + it("returns undefined when required fields are missing", async () => { + const provider = createMockProvider(true) + + const result = await handleDeleteSkill(provider, { + type: "deleteSkill", + skillName: "test-skill", + // missing source + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(mockLog).toHaveBeenCalledWith("Error deleting skill: Missing required fields: skillName or source") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to delete skill: Missing required fields: skillName or source", + ) + }) + + it("returns undefined when skills manager is not available", async () => { + const provider = createMockProvider(false) + + const result = await handleDeleteSkill(provider, { + type: "deleteSkill", + skillName: "test-skill", + source: "global", + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(mockLog).toHaveBeenCalledWith("Error deleting skill: Skills manager not available") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to delete skill: Skills manager not available", + ) + }) + }) + + 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) + mockGetSkill.mockReturnValue(mockSkills[0]) + + await handleOpenSkillFile(provider, { + type: "openSkillFile", + skillName: "test-skill", + source: "global", + } as WebviewMessage) + + expect(mockGetSkill).toHaveBeenCalledWith("test-skill", "global", undefined) + expect(openFile).toHaveBeenCalledWith("/path/to/test-skill/SKILL.md") + }) + + it("opens a skill file with mode restriction", async () => { + const provider = createMockProvider(true) + mockGetSkill.mockReturnValue(mockSkills[1]) + + await handleOpenSkillFile(provider, { + type: "openSkillFile", + skillName: "project-skill", + source: "project", + skillMode: "code", + } as WebviewMessage) + + expect(mockGetSkill).toHaveBeenCalledWith("project-skill", "project", "code") + expect(openFile).toHaveBeenCalledWith("/project/.roo/skills/project-skill/SKILL.md") + }) + + it("shows error when required fields are missing", async () => { + const provider = createMockProvider(true) + + await handleOpenSkillFile(provider, { + type: "openSkillFile", + skillName: "test-skill", + // missing source + } as WebviewMessage) + + expect(mockLog).toHaveBeenCalledWith( + "Error opening skill file: Missing required fields: skillName or source", + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to open skill file: Missing required fields: skillName or source", + ) + }) + + it("shows error when skills manager is not available", async () => { + const provider = createMockProvider(false) + + await handleOpenSkillFile(provider, { + type: "openSkillFile", + skillName: "test-skill", + source: "global", + } as WebviewMessage) + + expect(mockLog).toHaveBeenCalledWith("Error opening skill file: Skills manager not available") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to open skill file: Skills manager not available", + ) + }) + + it("shows error when skill is not found", async () => { + const provider = createMockProvider(true) + mockGetSkill.mockReturnValue(undefined) + + await handleOpenSkillFile(provider, { + type: "openSkillFile", + skillName: "nonexistent-skill", + source: "global", + } as WebviewMessage) + + expect(mockLog).toHaveBeenCalledWith('Error opening skill file: Skill "nonexistent-skill" not found') + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Failed to open skill file: Skill "nonexistent-skill" not found', + ) + }) + }) +}) diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts new file mode 100644 index 00000000000..f09f22f58c5 --- /dev/null +++ b/src/core/webview/skillsMessageHandler.ts @@ -0,0 +1,189 @@ +import * as vscode from "vscode" + +import type { SkillMetadata, WebviewMessage } from "@roo-code/types" + +import type { ClineProvider } from "./ClineProvider" +import { openFile } from "../../integrations/misc/open-file" +import { t } from "../../i18n" + +/** + * Handles the requestSkills message - returns all skills metadata + */ +export async function handleRequestSkills(provider: ClineProvider): Promise { + try { + const skillsManager = provider.getSkillsManager() + if (skillsManager) { + const skills = skillsManager.getSkillsMetadata() + await provider.postMessageToWebview({ type: "skills", skills }) + return skills + } else { + await provider.postMessageToWebview({ type: "skills", skills: [] }) + return [] + } + } catch (error) { + provider.log(`Error fetching skills: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + await provider.postMessageToWebview({ type: "skills", skills: [] }) + return [] + } +} + +/** + * Handles the createSkill message - creates a new skill + */ +export async function handleCreateSkill( + provider: ClineProvider, + message: WebviewMessage, +): Promise { + try { + const skillName = message.skillName + const source = message.source + const skillDescription = message.skillDescription + const skillMode = message.skillMode + + if (!skillName || !source || !skillDescription) { + throw new Error(t("skills:errors.missing_create_fields")) + } + + // Built-in skills cannot be created + 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")) + } + + const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, skillMode) + + // Open the created file in the editor + openFile(createdPath) + + // 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 creating skill: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to create skill: ${errorMessage}`) + return undefined + } +} + +/** + * Handles the deleteSkill message - deletes a skill + */ +export async function handleDeleteSkill( + provider: ClineProvider, + message: WebviewMessage, +): Promise { + try { + const skillName = message.skillName + const source = message.source + const skillMode = message.skillMode + + if (!skillName || !source) { + throw new Error(t("skills:errors.missing_delete_fields")) + } + + // Built-in skills cannot be deleted + 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.deleteSkill(skillName, source, skillMode) + + // 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 deleting skill: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to delete skill: ${errorMessage}`) + return undefined + } +} + +/** + * 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 + */ +export async function handleOpenSkillFile(provider: ClineProvider, message: WebviewMessage): Promise { + try { + const skillName = message.skillName + const source = message.source + const skillMode = message.skillMode + + if (!skillName || !source) { + throw new Error(t("skills:errors.missing_delete_fields")) + } + + // Built-in skills cannot be opened as files (they have no file path) + if (source === "built-in") { + throw new Error(t("skills:errors.cannot_open_builtin")) + } + + const skillsManager = provider.getSkillsManager() + if (!skillsManager) { + throw new Error(t("skills:errors.manager_unavailable")) + } + + const skill = skillsManager.getSkill(skillName, source, skillMode) + if (!skill) { + throw new Error(t("skills:errors.skill_not_found", { name: skillName })) + } + + openFile(skill.path) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Error opening skill file: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to open skill file: ${errorMessage}`) + } +} diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index b66e3403f7c..dfb604eb865 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -32,6 +32,13 @@ import { ClineProvider } from "./ClineProvider" import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" import { generateErrorDiagnostics } from "./diagnosticsHandler" +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" @@ -2984,6 +2991,26 @@ export const webviewMessageHandler = async ( } break } + case "requestSkills": { + await handleRequestSkills(provider) + break + } + case "createSkill": { + await handleCreateSkill(provider, message) + break + } + case "deleteSkill": { + await handleDeleteSkill(provider, message) + break + } + case "moveSkill": { + await handleMoveSkill(provider, message) + break + } + case "openSkillFile": { + await handleOpenSkillFile(provider, message) + break + } case "openCommandFile": { try { if (message.text) { diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json new file mode 100644 index 00000000000..74d8cba0393 --- /dev/null +++ b/src/i18n/locales/ca/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "El nom de l'habilitat ha de tenir entre 1 i {{maxLength}} caràcters (s'han rebut {{length}})", + "name_format": "El nom de l'habilitat només pot contenir lletres minúscules, números i guions (sense guions inicials o finals, sense guions consecutius)", + "description_length": "La descripció de l'habilitat ha de tenir entre 1 i 1024 caràcters (s'han rebut {{length}})", + "no_workspace": "No es pot crear l'habilitat del projecte: no hi ha cap carpeta d'espai de treball oberta", + "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}}\"", + "cannot_modify_builtin": "Les habilitats integrades no es poden crear ni eliminar", + "cannot_open_builtin": "Les habilitats integrades no es poden obrir com a fitxers" + } +} diff --git a/src/i18n/locales/de/skills.json b/src/i18n/locales/de/skills.json new file mode 100644 index 00000000000..5aad37950f7 --- /dev/null +++ b/src/i18n/locales/de/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Skill-Name muss 1-{{maxLength}} Zeichen lang sein (erhalten: {{length}})", + "name_format": "Skill-Name darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten (keine führenden oder nachgestellten Bindestriche, keine aufeinanderfolgenden Bindestriche)", + "description_length": "Skill-Beschreibung muss 1-1024 Zeichen lang sein (erhalten: {{length}})", + "no_workspace": "Projekt-Skill kann nicht erstellt werden: kein Workspace-Ordner ist geöffnet", + "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", + "cannot_modify_builtin": "Integrierte Skills können nicht erstellt oder gelöscht werden", + "cannot_open_builtin": "Integrierte Skills können nicht als Dateien geöffnet werden" + } +} diff --git a/src/i18n/locales/en/skills.json b/src/i18n/locales/en/skills.json new file mode 100644 index 00000000000..ef4d7e68e3d --- /dev/null +++ b/src/i18n/locales/en/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Skill name must be 1-{{maxLength}} characters (got {{length}})", + "name_format": "Skill name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)", + "description_length": "Skill description must be 1-1024 characters (got {{length}})", + "no_workspace": "Cannot create project skill: no workspace folder is open", + "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, deleted, or moved", + "cannot_open_builtin": "Built-in skills cannot be opened as files" + } +} diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json new file mode 100644 index 00000000000..65345815182 --- /dev/null +++ b/src/i18n/locales/es/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "El nombre de la habilidad debe tener entre 1 y {{maxLength}} caracteres (se recibieron {{length}})", + "name_format": "El nombre de la habilidad solo puede contener letras minúsculas, números y guiones (sin guiones al inicio o al final, sin guiones consecutivos)", + "description_length": "La descripción de la habilidad debe tener entre 1 y 1024 caracteres (se recibieron {{length}})", + "no_workspace": "No se puede crear la habilidad del proyecto: no hay ninguna carpeta de espacio de trabajo abierta", + "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}}\"", + "cannot_modify_builtin": "Las habilidades integradas no se pueden crear ni eliminar", + "cannot_open_builtin": "Las habilidades integradas no se pueden abrir como archivos" + } +} diff --git a/src/i18n/locales/fr/skills.json b/src/i18n/locales/fr/skills.json new file mode 100644 index 00000000000..5c4cb1f5ae0 --- /dev/null +++ b/src/i18n/locales/fr/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Le nom de la compétence doit contenir entre 1 et {{maxLength}} caractères ({{length}} reçu)", + "name_format": "Le nom de la compétence ne peut contenir que des lettres minuscules, des chiffres et des traits d'union (pas de trait d'union initial ou final, pas de traits d'union consécutifs)", + "description_length": "La description de la compétence doit contenir entre 1 et 1024 caractères ({{length}} reçu)", + "no_workspace": "Impossible de créer la compétence de projet : aucun dossier d'espace de travail n'est ouvert", + "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", + "cannot_modify_builtin": "Les compétences intégrées ne peuvent pas être créées ou supprimées", + "cannot_open_builtin": "Les compétences intégrées ne peuvent pas être ouvertes en tant que fichiers" + } +} diff --git a/src/i18n/locales/hi/skills.json b/src/i18n/locales/hi/skills.json new file mode 100644 index 00000000000..50929b48459 --- /dev/null +++ b/src/i18n/locales/hi/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "स्किल का नाम 1-{{maxLength}} वर्णों का होना चाहिए ({{length}} प्राप्त हुआ)", + "name_format": "स्किल के नाम में केवल छोटे अक्षर, संख्याएं और हाइफ़न हो सकते हैं (शुरुआत या अंत में हाइफ़न नहीं, लगातार हाइफ़न नहीं)", + "description_length": "स्किल का विवरण 1-1024 वर्णों का होना चाहिए ({{length}} प्राप्त हुआ)", + "no_workspace": "प्रोजेक्ट स्किल नहीं बनाया जा सकता: कोई वर्कस्पेस फ़ोल्डर खुला नहीं है", + "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}}\" नहीं मिला", + "cannot_modify_builtin": "बिल्ट-इन स्किल्स को बनाया या हटाया नहीं जा सकता", + "cannot_open_builtin": "बिल्ट-इन स्किल्स को फाइलों के रूप में नहीं खोला जा सकता" + } +} diff --git a/src/i18n/locales/id/skills.json b/src/i18n/locales/id/skills.json new file mode 100644 index 00000000000..cfa01b33234 --- /dev/null +++ b/src/i18n/locales/id/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Nama skill harus 1-{{maxLength}} karakter (diterima {{length}})", + "name_format": "Nama skill hanya boleh berisi huruf kecil, angka, dan tanda hubung (tanpa tanda hubung di awal atau akhir, tanpa tanda hubung berturut-turut)", + "description_length": "Deskripsi skill harus 1-1024 karakter (diterima {{length}})", + "no_workspace": "Tidak dapat membuat skill proyek: tidak ada folder workspace yang terbuka", + "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", + "cannot_modify_builtin": "Skill bawaan tidak dapat dibuat atau dihapus", + "cannot_open_builtin": "Skill bawaan tidak dapat dibuka sebagai file" + } +} diff --git a/src/i18n/locales/it/skills.json b/src/i18n/locales/it/skills.json new file mode 100644 index 00000000000..0ddcf0f70c0 --- /dev/null +++ b/src/i18n/locales/it/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Il nome della skill deve essere di 1-{{maxLength}} caratteri (ricevuti {{length}})", + "name_format": "Il nome della skill può contenere solo lettere minuscole, numeri e trattini (senza trattini iniziali o finali, senza trattini consecutivi)", + "description_length": "La descrizione della skill deve essere di 1-1024 caratteri (ricevuti {{length}})", + "no_workspace": "Impossibile creare la skill del progetto: nessuna cartella di workspace aperta", + "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", + "cannot_modify_builtin": "Le skill integrate non possono essere create o eliminate", + "cannot_open_builtin": "Le skill integrate non possono essere aperte come file" + } +} diff --git a/src/i18n/locales/ja/skills.json b/src/i18n/locales/ja/skills.json new file mode 100644 index 00000000000..16576b2be35 --- /dev/null +++ b/src/i18n/locales/ja/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "スキル名は1-{{maxLength}}文字である必要があります({{length}}文字を受信)", + "name_format": "スキル名には小文字、数字、ハイフンのみ使用できます(先頭または末尾のハイフン、連続するハイフンは不可)", + "description_length": "スキルの説明は1-1024文字である必要があります({{length}}文字を受信)", + "no_workspace": "プロジェクトスキルを作成できません:ワークスペースフォルダが開かれていません", + "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}}」が見つかりません", + "cannot_modify_builtin": "組み込みスキルは作成または削除できません", + "cannot_open_builtin": "組み込みスキルはファイルとして開けません" + } +} diff --git a/src/i18n/locales/ko/skills.json b/src/i18n/locales/ko/skills.json new file mode 100644 index 00000000000..c5808f3630b --- /dev/null +++ b/src/i18n/locales/ko/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "스킬 이름은 1-{{maxLength}}자여야 합니다({{length}}자 수신됨)", + "name_format": "스킬 이름은 소문자, 숫자, 하이픈만 포함할 수 있습니다(앞뒤 하이픈 없음, 연속 하이픈 없음)", + "description_length": "스킬 설명은 1-1024자여야 합니다({{length}}자 수신됨)", + "no_workspace": "프로젝트 스킬을 생성할 수 없습니다: 열린 작업 공간 폴더가 없습니다", + "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}}\"을(를) 찾을 수 없습니다", + "cannot_modify_builtin": "기본 제공 스킬은 생성하거나 삭제할 수 없습니다", + "cannot_open_builtin": "기본 제공 스킬은 파일로 열 수 없습니다" + } +} diff --git a/src/i18n/locales/nl/skills.json b/src/i18n/locales/nl/skills.json new file mode 100644 index 00000000000..6c6e7e0e832 --- /dev/null +++ b/src/i18n/locales/nl/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Vaardigheidsnaam moet 1-{{maxLength}} tekens lang zijn ({{length}} ontvangen)", + "name_format": "Vaardigheidsnaam mag alleen kleine letters, cijfers en koppeltekens bevatten (geen voorloop- of achterloop-koppeltekens, geen opeenvolgende koppeltekens)", + "description_length": "Vaardigheidsbeschrijving moet 1-1024 tekens lang zijn ({{length}} ontvangen)", + "no_workspace": "Kan projectvaardigheid niet aanmaken: geen werkruimtemap geopend", + "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", + "cannot_modify_builtin": "Ingebouwde vaardigheden kunnen niet worden aangemaakt of verwijderd", + "cannot_open_builtin": "Ingebouwde vaardigheden kunnen niet als bestanden worden geopend" + } +} diff --git a/src/i18n/locales/pl/skills.json b/src/i18n/locales/pl/skills.json new file mode 100644 index 00000000000..f9363e42d03 --- /dev/null +++ b/src/i18n/locales/pl/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Nazwa umiejętności musi mieć 1-{{maxLength}} znaków (otrzymano {{length}})", + "name_format": "Nazwa umiejętności może zawierać tylko małe litery, cyfry i myślniki (bez myślników na początku lub końcu, bez następujących po sobie myślników)", + "description_length": "Opis umiejętności musi mieć 1-1024 znaków (otrzymano {{length}})", + "no_workspace": "Nie można utworzyć umiejętności projektu: nie otwarto folderu obszaru roboczego", + "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}}\"", + "cannot_modify_builtin": "Wbudowane umiejętności nie mogą być tworzone ani usuwane", + "cannot_open_builtin": "Wbudowane umiejętności nie mogą być otwierane jako pliki" + } +} diff --git a/src/i18n/locales/pt-BR/skills.json b/src/i18n/locales/pt-BR/skills.json new file mode 100644 index 00000000000..8058e9f6a38 --- /dev/null +++ b/src/i18n/locales/pt-BR/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "O nome da habilidade deve ter de 1 a {{maxLength}} caracteres (recebido {{length}})", + "name_format": "O nome da habilidade só pode conter letras minúsculas, números e hifens (sem hifens iniciais ou finais, sem hifens consecutivos)", + "description_length": "A descrição da habilidade deve ter de 1 a 1024 caracteres (recebido {{length}})", + "no_workspace": "Não é possível criar habilidade do projeto: nenhuma pasta de espaço de trabalho está aberta", + "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", + "cannot_modify_builtin": "Habilidades integradas não podem ser criadas ou excluídas", + "cannot_open_builtin": "Habilidades integradas não podem ser abertas como arquivos" + } +} diff --git a/src/i18n/locales/ru/skills.json b/src/i18n/locales/ru/skills.json new file mode 100644 index 00000000000..627a8fd4d65 --- /dev/null +++ b/src/i18n/locales/ru/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Имя навыка должно быть от 1 до {{maxLength}} символов (получено {{length}})", + "name_format": "Имя навыка может содержать только строчные буквы, цифры и дефисы (без начальных или конечных дефисов, без последовательных дефисов)", + "description_length": "Описание навыка должно быть от 1 до 1024 символов (получено {{length}})", + "no_workspace": "Невозможно создать навык проекта: не открыта папка рабочего пространства", + "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}}\" не найден", + "cannot_modify_builtin": "Встроенные навыки нельзя создавать или удалять", + "cannot_open_builtin": "Встроенные навыки нельзя открыть как файлы" + } +} diff --git a/src/i18n/locales/tr/skills.json b/src/i18n/locales/tr/skills.json new file mode 100644 index 00000000000..e7781aa696b --- /dev/null +++ b/src/i18n/locales/tr/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Beceri adı 1-{{maxLength}} karakter olmalıdır ({{length}} alındı)", + "name_format": "Beceri adı yalnızca küçük harfler, rakamlar ve tire içerebilir (başta veya sonda tire yok, ardışık tire yok)", + "description_length": "Beceri açıklaması 1-1024 karakter olmalıdır ({{length}} alındı)", + "no_workspace": "Proje becerisi oluşturulamıyor: açık çalışma alanı klasörü yok", + "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ı", + "cannot_modify_builtin": "Yerleşik beceriler oluşturulamaz veya silinemez", + "cannot_open_builtin": "Yerleşik beceriler dosya olarak açılamaz" + } +} diff --git a/src/i18n/locales/vi/skills.json b/src/i18n/locales/vi/skills.json new file mode 100644 index 00000000000..f97b7ed2b07 --- /dev/null +++ b/src/i18n/locales/vi/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "Tên kỹ năng phải từ 1-{{maxLength}} ký tự (nhận được {{length}})", + "name_format": "Tên kỹ năng chỉ có thể chứa chữ cái thường, số và dấu gạch ngang (không có dấu gạch ngang đầu hoặc cuối, không có dấu gạch ngang liên tiếp)", + "description_length": "Mô tả kỹ năng phải từ 1-1024 ký tự (nhận được {{length}})", + "no_workspace": "Không thể tạo kỹ năng dự án: không có thư mục vùng làm việc nào được mở", + "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}}\"", + "cannot_modify_builtin": "Không thể tạo hoặc xóa kỹ năng tích hợp sẵn", + "cannot_open_builtin": "Không thể mở kỹ năng tích hợp sẵn dưới dạng tệp" + } +} diff --git a/src/i18n/locales/zh-CN/skills.json b/src/i18n/locales/zh-CN/skills.json new file mode 100644 index 00000000000..566f583feee --- /dev/null +++ b/src/i18n/locales/zh-CN/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "技能名称必须为 1-{{maxLength}} 个字符(收到 {{length}} 个)", + "name_format": "技能名称只能包含小写字母、数字和连字符(不能有前导或尾随连字符,不能有连续连字符)", + "description_length": "技能描述必须为 1-1024 个字符(收到 {{length}} 个)", + "no_workspace": "无法创建项目技能:未打开工作区文件夹", + "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}}\"", + "cannot_modify_builtin": "内置技能无法创建或删除", + "cannot_open_builtin": "内置技能无法作为文件打开" + } +} diff --git a/src/i18n/locales/zh-TW/skills.json b/src/i18n/locales/zh-TW/skills.json new file mode 100644 index 00000000000..633bb1a6b2b --- /dev/null +++ b/src/i18n/locales/zh-TW/skills.json @@ -0,0 +1,17 @@ +{ + "errors": { + "name_length": "技能名稱必須為 1-{{maxLength}} 個字元(收到 {{length}} 個)", + "name_format": "技能名稱只能包含小寫字母、數字和連字號(不能有前導或尾隨連字號,不能有連續連字號)", + "description_length": "技能描述必須為 1-1024 個字元(收到 {{length}} 個)", + "no_workspace": "無法建立專案技能:未開啟工作區資料夾", + "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}}」", + "cannot_modify_builtin": "內建技能無法建立或刪除", + "cannot_open_builtin": "內建技能無法作為檔案開啟" + } +} diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts index 7e8e9028622..e23e2e1a390 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -265,6 +265,217 @@ export class SkillsManager { } } + /** + * Get all skills metadata (for UI display) + * Returns skills from all sources without content + */ + getSkillsMetadata(): SkillMetadata[] { + return this.getAllSkills() + } + + /** + * Get a skill by name, source, and optionally mode + */ + getSkill(name: string, source: "global" | "project", mode?: string): SkillMetadata | undefined { + const skillKey = this.getSkillKey(name, source, mode) + return this.skills.get(skillKey) + } + + /** + * Validate skill name per agentskills.io spec using shared validation. + * Converts error codes to user-friendly error messages. + */ + private validateSkillName(name: string): { valid: boolean; error?: string } { + const result = validateSkillNameShared(name) + if (!result.valid) { + return { valid: false, error: this.getSkillNameErrorMessage(name, result.error!) } + } + return { valid: true } + } + + /** + * Convert skill name validation error code to a user-friendly error message. + */ + private getSkillNameErrorMessage(name: string, error: SkillNameValidationError): string { + switch (error) { + case SkillNameValidationError.Empty: + return t("skills:errors.name_length", { maxLength: SKILL_NAME_MAX_LENGTH, length: name.length }) + case SkillNameValidationError.TooLong: + return t("skills:errors.name_length", { maxLength: SKILL_NAME_MAX_LENGTH, length: name.length }) + case SkillNameValidationError.InvalidFormat: + return t("skills:errors.name_format") + } + } + + /** + * Create a new skill + * @param name - Skill name (must be valid per agentskills.io spec) + * @param source - "global" or "project" + * @param description - Skill description + * @param mode - Optional mode restriction (creates in skills-{mode}/ directory) + * @returns Path to created SKILL.md file + */ + async createSkill(name: string, source: "global" | "project", description: string, mode?: string): Promise { + // Validate skill name + const validation = this.validateSkillName(name) + if (!validation.valid) { + throw new Error(validation.error) + } + + // Validate description + const trimmedDescription = description.trim() + if (trimmedDescription.length < 1 || trimmedDescription.length > 1024) { + throw new Error(t("skills:errors.description_length", { length: trimmedDescription.length })) + } + + // 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 skills directory (with optional mode suffix) + const skillsDirName = mode ? `skills-${mode}` : "skills" + const skillsDir = path.join(baseDir, skillsDirName) + const skillDir = path.join(skillsDir, name) + const skillMdPath = path.join(skillDir, "SKILL.md") + + // Check if skill already exists + if (await fileExists(skillMdPath)) { + throw new Error(t("skills:errors.already_exists", { name, path: skillMdPath })) + } + + // Create the skill directory + await fs.mkdir(skillDir, { recursive: true }) + + // Generate SKILL.md content with frontmatter + const titleName = name + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + + const skillContent = `--- +name: ${name} +description: ${trimmedDescription} +--- + +# ${titleName} + +## Instructions + +Add your skill instructions here. +` + + // Write the SKILL.md file + await fs.writeFile(skillMdPath, skillContent, "utf-8") + + // Refresh skills list + await this.discoverSkills() + + return skillMdPath + } + + /** + * Delete a skill + * @param name - Skill name to delete + * @param source - Where the skill is located + * @param mode - Optional mode (to locate in skills-{mode}/ directory) + */ + async deleteSkill(name: string, source: "global" | "project", mode?: string): Promise { + // Find the skill + const skill = this.getSkill(name, source, mode) + if (!skill) { + const modeInfo = mode ? ` (mode: ${mode})` : "" + throw new Error(t("skills:errors.not_found", { name, source, modeInfo })) + } + + // Get the skill directory (parent of SKILL.md) + const skillDir = path.dirname(skill.path) + + // Delete the entire skill directory + await fs.rm(skillDir, { recursive: true, force: true }) + + // Refresh skills list + 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)) { + 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 89024432b12..189ca41b51a 100644 --- a/src/services/skills/__tests__/SkillsManager.spec.ts +++ b/src/services/skills/__tests__/SkillsManager.spec.ts @@ -1,16 +1,33 @@ import * as path from "path" // Use vi.hoisted to ensure mocks are available during hoisting -const { mockStat, mockReadFile, mockReaddir, mockHomedir, mockDirectoryExists, mockFileExists, mockRealpath } = - vi.hoisted(() => ({ - mockStat: vi.fn(), - mockReadFile: vi.fn(), - mockReaddir: vi.fn(), - mockHomedir: vi.fn(), - mockDirectoryExists: vi.fn(), - mockFileExists: vi.fn(), - mockRealpath: vi.fn(), - })) +const { + mockStat, + mockReadFile, + mockReaddir, + mockHomedir, + mockDirectoryExists, + mockFileExists, + mockRealpath, + mockMkdir, + mockWriteFile, + mockRm, + mockRename, + mockRmdir, +} = vi.hoisted(() => ({ + mockStat: vi.fn(), + mockReadFile: vi.fn(), + mockReaddir: vi.fn(), + mockHomedir: vi.fn(), + mockDirectoryExists: vi.fn(), + mockFileExists: vi.fn(), + mockRealpath: vi.fn(), + mockMkdir: vi.fn(), + mockWriteFile: vi.fn(), + mockRm: vi.fn(), + mockRename: vi.fn(), + mockRmdir: vi.fn(), +})) // Platform-agnostic test paths // Use forward slashes for consistency, then normalize with path.normalize @@ -28,11 +45,21 @@ vi.mock("fs/promises", () => ({ readFile: mockReadFile, readdir: mockReaddir, realpath: mockRealpath, + mkdir: mockMkdir, + writeFile: mockWriteFile, + rm: mockRm, + rename: mockRename, + rmdir: mockRmdir, }, stat: mockStat, readFile: mockReadFile, readdir: mockReaddir, realpath: mockRealpath, + mkdir: mockMkdir, + writeFile: mockWriteFile, + rm: mockRm, + rename: mockRename, + rmdir: mockRmdir, })) // Mock os module @@ -1053,4 +1080,666 @@ description: A test skill expect(skills).toHaveLength(0) }) }) + + describe("getSkillsMetadata", () => { + it("should return all skills metadata", async () => { + const testSkillDir = p(globalSkillsDir, "test-skill") + const testSkillMd = p(testSkillDir, "SKILL.md") + + 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 === testSkillDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + return file === testSkillMd + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + await skillsManager.discoverSkills() + + const metadata = skillsManager.getSkillsMetadata() + + expect(metadata).toHaveLength(1) + expect(metadata[0].name).toBe("test-skill") + expect(metadata[0].description).toBe("A test skill") + }) + }) + + describe("getSkill", () => { + it("should return a skill by name, source, and mode", async () => { + const testSkillDir = p(globalSkillsDir, "test-skill") + const testSkillMd = p(testSkillDir, "SKILL.md") + + 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 === testSkillDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + return file === testSkillMd + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + await skillsManager.discoverSkills() + + const skill = skillsManager.getSkill("test-skill", "global") + + expect(skill).toBeDefined() + expect(skill?.name).toBe("test-skill") + expect(skill?.source).toBe("global") + }) + + it("should return undefined for non-existent skill", async () => { + mockDirectoryExists.mockResolvedValue(false) + mockRealpath.mockImplementation(async (p: string) => p) + mockReaddir.mockResolvedValue([]) + + await skillsManager.discoverSkills() + + const skill = skillsManager.getSkill("non-existent", "global") + + expect(skill).toBeUndefined() + }) + }) + + describe("createSkill", () => { + it("should create a new global skill", async () => { + // Setup: no existing skills + mockDirectoryExists.mockResolvedValue(false) + mockRealpath.mockImplementation(async (p: string) => p) + mockReaddir.mockResolvedValue([]) + mockFileExists.mockResolvedValue(false) + mockMkdir.mockResolvedValue(undefined) + mockWriteFile.mockResolvedValue(undefined) + + const createdPath = await skillsManager.createSkill("new-skill", "global", "A new skill description") + + expect(createdPath).toBe(p(GLOBAL_ROO_DIR, "skills", "new-skill", "SKILL.md")) + expect(mockMkdir).toHaveBeenCalledWith(p(GLOBAL_ROO_DIR, "skills", "new-skill"), { recursive: true }) + expect(mockWriteFile).toHaveBeenCalled() + + // Verify the content written + const writeCall = mockWriteFile.mock.calls[0] + expect(writeCall[0]).toBe(p(GLOBAL_ROO_DIR, "skills", "new-skill", "SKILL.md")) + expect(writeCall[1]).toContain("name: new-skill") + expect(writeCall[1]).toContain("description: A new skill description") + }) + + it("should create a mode-specific skill", async () => { + mockDirectoryExists.mockResolvedValue(false) + mockRealpath.mockImplementation(async (p: string) => p) + mockReaddir.mockResolvedValue([]) + mockFileExists.mockResolvedValue(false) + mockMkdir.mockResolvedValue(undefined) + mockWriteFile.mockResolvedValue(undefined) + + const createdPath = await skillsManager.createSkill("code-skill", "global", "A code skill", "code") + + expect(createdPath).toBe(p(GLOBAL_ROO_DIR, "skills-code", "code-skill", "SKILL.md")) + }) + + it("should create a project skill", async () => { + mockDirectoryExists.mockResolvedValue(false) + mockRealpath.mockImplementation(async (p: string) => p) + mockReaddir.mockResolvedValue([]) + mockFileExists.mockResolvedValue(false) + mockMkdir.mockResolvedValue(undefined) + mockWriteFile.mockResolvedValue(undefined) + + const createdPath = await skillsManager.createSkill("project-skill", "project", "A project skill") + + expect(createdPath).toBe(p(PROJECT_DIR, ".roo", "skills", "project-skill", "SKILL.md")) + }) + + it("should throw error for invalid skill name", async () => { + await expect(skillsManager.createSkill("Invalid-Name", "global", "Description")).rejects.toThrow( + "Skill name must be lowercase letters/numbers/hyphens only", + ) + }) + + it("should throw error for skill name that is too long", async () => { + const longName = "a".repeat(65) + await expect(skillsManager.createSkill(longName, "global", "Description")).rejects.toThrow( + "Skill name must be 1-64 characters", + ) + }) + + it("should throw error for skill name starting with hyphen", async () => { + await expect(skillsManager.createSkill("-invalid", "global", "Description")).rejects.toThrow( + "Skill name must be lowercase letters/numbers/hyphens only", + ) + }) + + it("should throw error for skill name ending with hyphen", async () => { + await expect(skillsManager.createSkill("invalid-", "global", "Description")).rejects.toThrow( + "Skill name must be lowercase letters/numbers/hyphens only", + ) + }) + + it("should throw error for skill name with consecutive hyphens", async () => { + await expect(skillsManager.createSkill("invalid--name", "global", "Description")).rejects.toThrow( + "Skill name must be lowercase letters/numbers/hyphens only", + ) + }) + + it("should throw error for empty description", async () => { + await expect(skillsManager.createSkill("valid-name", "global", " ")).rejects.toThrow( + "Skill description must be 1-1024 characters", + ) + }) + + it("should throw error for description that is too long", async () => { + const longDesc = "d".repeat(1025) + await expect(skillsManager.createSkill("valid-name", "global", longDesc)).rejects.toThrow( + "Skill description must be 1-1024 characters", + ) + }) + + it("should throw error if skill already exists", async () => { + mockFileExists.mockResolvedValue(true) + + await expect(skillsManager.createSkill("existing-skill", "global", "Description")).rejects.toThrow( + "already exists", + ) + }) + }) + + describe("deleteSkill", () => { + it("should delete an existing skill", async () => { + const testSkillDir = p(globalSkillsDir, "test-skill") + const testSkillMd = p(testSkillDir, "SKILL.md") + + // Setup: skill exists + 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 === testSkillDir) { + return { isDirectory: () => true } + } + throw new Error("Not found") + }) + + mockFileExists.mockImplementation(async (file: string) => { + return file === testSkillMd + }) + + mockReadFile.mockResolvedValue(`--- +name: test-skill +description: A test skill +--- +Instructions`) + + mockRm.mockResolvedValue(undefined) + + await skillsManager.discoverSkills() + + // Verify skill exists + expect(skillsManager.getSkill("test-skill", "global")).toBeDefined() + + // Delete the skill + await skillsManager.deleteSkill("test-skill", "global") + + expect(mockRm).toHaveBeenCalledWith(testSkillDir, { recursive: true, force: true }) + }) + + 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.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 new file mode 100644 index 00000000000..cd11f4553d4 --- /dev/null +++ b/webview-ui/src/components/settings/SkillItem.tsx @@ -0,0 +1,124 @@ +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 { 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 + onEdit: () => void + onDelete: () => void +} + +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 ( +
+ {/* Skill name and description */} +
+
+ {skill.name} +
+ {skill.description && ( +
{skill.description}
+ )} +
+ + {/* Mode dropdown */} +
+ {isBuiltIn ? ( + + {skill.mode || t("settings:skills.modeAny")} + + ) : ( + + + + )} +
+ + {/* Action buttons */} +
+ + + + + {!isBuiltIn && ( + + + + )} +
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx new file mode 100644 index 00000000000..a8406e62944 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx @@ -0,0 +1,257 @@ +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", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock the translation hook +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + 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 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) => ( + + ), + StandardTooltip: ({ children, content }: any) => ( +
+ {children} +
+ ), + Select: ({ children, value, onValueChange }: any) => ( +
+ {children} + +
+ ), + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) => ( +
+ {children} +
+ ), + SelectTrigger: ({ children, className }: any) => ( + + ), + SelectValue: () => Value, +})) + +const mockSkill: SkillMetadata = { + name: "test-skill", + description: "A test skill description", + path: "/path/to/skill/SKILL.md", + source: "project", +} + +const mockSkillWithMode: SkillMetadata = { + name: "mode-specific-skill", + description: "A mode-specific skill", + path: "/path/to/skill/SKILL.md", + source: "global", + 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() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders skill name", () => { + render() + + expect(screen.getByText("test-skill")).toBeInTheDocument() + }) + + it("renders skill description", () => { + render() + + expect(screen.getByText("A test skill description")).toBeInTheDocument() + }) + + it("renders mode dropdown for non-built-in skills", () => { + render() + + expect(screen.getByTestId("select")).toBeInTheDocument() + }) + + it("renders mode dropdown with correct current value", () => { + render() + + const select = screen.getByTestId("select") + expect(select).toHaveAttribute("data-value", "architect") + }) + + it("renders mode dropdown with __any__ for skills without mode", () => { + render() + + 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", () => { + render() + + const buttons = screen.getAllByTestId("button") + // First button is edit + fireEvent.click(buttons[0]) + + expect(mockOnEdit).toHaveBeenCalledTimes(1) + }) + + it("calls onDelete when delete button is clicked for non-built-in skills", () => { + render() + + const buttons = screen.getAllByTestId("button") + // 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() + + const nameElement = screen.getByText("test-skill") + fireEvent.click(nameElement) + + 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", + description: "", + path: "/path/to/skill/SKILL.md", + source: "project", + } + + render() + + expect(screen.getByText("no-desc-skill")).toBeInTheDocument() + // Description div should not be rendered when empty + expect(screen.queryByText("A test skill description")).not.toBeInTheDocument() + }) + + it("renders with proper styling classes", () => { + const { container } = render() + + const itemDiv = container.firstChild + expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground") + }) + + 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/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index bfc87995974..7a4e0019c59 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Experimental", "language": "Idioma", - "about": "Sobre Roo Code" + "about": "Sobre Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Obtenir clau API d'Anthropic", "anthropicUseAuthToken": "Passar la clau API d'Anthropic com a capçalera d'autorització en lloc de X-Api-Key", "anthropic1MContextBetaLabel": "Activa la finestra de context d'1M (Beta)", - "anthropic1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Activa la finestra de context d'1M (Beta)", - "awsBedrock1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4", "vertex1MContextBetaLabel": "Activa la finestra de context d'1M (Beta)", - "vertex1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4", "basetenApiKey": "Clau API de Baseten", "getBasetenApiKey": "Obtenir clau API de Baseten", "cerebrasApiKey": "Clau API de Cerebras", @@ -992,5 +993,52 @@ "label": "Requereix {{primaryMod}}+Intro per enviar missatges", "description": "Quan estigui activat, has de prémer {{primaryMod}}+Intro per enviar missatges en lloc de només Intro" } + }, + "skills": { + "description": "Gestiona les skills que proporcionen instruccions contextuals a l'agent. Les skills s'apliquen automàticament quan són rellevants per a les teves tasques. Més informació", + "projectSkills": "Skills del Projecte", + "globalSkills": "Skills Globals", + "noProjectSkills": "No hi ha skills de projecte configurades. Crea'n una per afegir capacitats específiques del projecte a l'agent.", + "noGlobalSkills": "No hi ha skills globals configurades. Crea'n una per afegir capacitats a l'agent disponibles en tots els projectes.", + "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.", + "confirm": "Eliminar", + "cancel": "Cancel·lar" + }, + "createDialog": { + "title": "Crear Nova Skill", + "description": "Defineix una nova plantilla de skill que proporcioni instruccions contextuals a l'agent.", + "nameLabel": "Nom", + "namePlaceholder": "el-meu-nom-de-skill", + "nameHint": "Només lletres minúscules, números i guions (1-64 caràcters)", + "descriptionLabel": "Descripció", + "descriptionPlaceholder": "Descriu quan s'hauria d'utilitzar aquesta skill...", + "descriptionHint": "Explica què fa aquesta skill i quan l'agent hauria d'aplicar-la (1-1024 caràcters)", + "sourceLabel": "Ubicació", + "sourceHint": "Tria si aquesta skill està disponible globalment o només en aquest projecte", + "modeLabel": "Mode (opcional)", + "modePlaceholder": "Qualsevol mode", + "modeHint": "Restringeix aquesta skill a un mode específic", + "modeAny": "Qualsevol mode", + "create": "Crear", + "cancel": "Cancel·lar" + }, + "source": { + "global": "Global (disponible en tots els projectes)", + "project": "Projecte (només aquest espai de treball)" + }, + "validation": { + "nameRequired": "El nom és obligatori", + "nameTooLong": "El nom ha de tenir com a màxim 64 caràcters", + "nameInvalid": "El nom ha de tenir entre 1 i 64 caràcters, només lletres minúscules, números o guions", + "descriptionRequired": "La descripció és obligatòria", + "descriptionTooLong": "La descripció ha de tenir com a màxim 1024 caràcters" + } } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index f2e5b7cb66e..d5182077809 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Experimentell", "language": "Sprache", - "about": "Über Roo Code" + "about": "Über Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -340,11 +341,11 @@ "getAnthropicApiKey": "Anthropic API-Schlüssel erhalten", "anthropicUseAuthToken": "Anthropic API-Schlüssel als Authorization-Header anstelle von X-Api-Key übergeben", "anthropic1MContextBetaLabel": "1M Kontextfenster aktivieren (Beta)", - "anthropic1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 / 4.5 / Claude Opus 4.6 auf 1 Million Token", + "anthropic1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 auf 1 Million Token", "awsBedrock1MContextBetaLabel": "1M Kontextfenster aktivieren (Beta)", - "awsBedrock1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 / 4.5 / Claude Opus 4.6 auf 1 Million Token", + "awsBedrock1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 auf 1 Million Token", "vertex1MContextBetaLabel": "1M Kontextfenster aktivieren (Beta)", - "vertex1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 / 4.5 / Claude Opus 4.6 auf 1 Million Token", + "vertex1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4 auf 1 Million Token", "basetenApiKey": "Baseten API-Schlüssel", "getBasetenApiKey": "Baseten API-Schlüssel erhalten", "cerebrasApiKey": "Cerebras API-Schlüssel", @@ -992,5 +993,52 @@ "label": "{{primaryMod}}+Enter zum Senden erfordern", "description": "Wenn aktiviert, musst du {{primaryMod}}+Enter drücken, um Nachrichten zu senden, anstatt nur Enter" } + }, + "skills": { + "description": "Verwalten Sie Skills, die dem Agenten kontextbezogene Anweisungen bereitstellen. Skills werden automatisch angewendet, wenn sie für Ihre Aufgaben relevant sind. Mehr erfahren", + "projectSkills": "Projekt-Skills", + "globalSkills": "Globale Skills", + "noProjectSkills": "Keine Projekt-Skills konfiguriert. Erstellen Sie eine, um projektspezifische Agentenfähigkeiten hinzuzufügen.", + "noGlobalSkills": "Keine globalen Skills konfiguriert. Erstellen Sie eine, um Agentenfähigkeiten hinzuzufügen, die in allen Projekten verfügbar sind.", + "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.", + "confirm": "Löschen", + "cancel": "Abbrechen" + }, + "createDialog": { + "title": "Neue Skill erstellen", + "description": "Definieren Sie eine neue Skill-Vorlage, die dem Agenten kontextbezogene Anweisungen bereitstellt.", + "nameLabel": "Name", + "namePlaceholder": "mein-skill-name", + "nameHint": "Nur Kleinbuchstaben, Zahlen und Bindestriche (1-64 Zeichen)", + "descriptionLabel": "Beschreibung", + "descriptionPlaceholder": "Beschreiben Sie, wann diese Skill verwendet werden sollte...", + "descriptionHint": "Erklären Sie, was diese Skill tut und wann der Agent sie anwenden sollte (1-1024 Zeichen)", + "sourceLabel": "Standort", + "sourceHint": "Wählen Sie, ob diese Skill global oder nur in diesem Projekt verfügbar ist", + "modeLabel": "Modus (optional)", + "modePlaceholder": "Beliebiger Modus", + "modeHint": "Beschränken Sie diese Skill auf einen bestimmten Modus", + "modeAny": "Beliebiger Modus", + "create": "Erstellen", + "cancel": "Abbrechen" + }, + "source": { + "global": "Global (in allen Projekten verfügbar)", + "project": "Projekt (nur dieser Arbeitsbereich)" + }, + "validation": { + "nameRequired": "Name ist erforderlich", + "nameTooLong": "Name darf höchstens 64 Zeichen lang sein", + "nameInvalid": "Name muss 1-64 Kleinbuchstaben, Zahlen oder Bindestriche enthalten", + "descriptionRequired": "Beschreibung ist erforderlich", + "descriptionTooLong": "Beschreibung darf höchstens 1024 Zeichen lang sein" + } } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index ad3ea5b6e63..de76ba4c679 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -30,6 +30,7 @@ "modes": "Modes", "mcp": "MCP Servers", "worktrees": "Worktrees", + "skills": "Skills", "autoApprove": "Auto-Approve", "browser": "Browser", "checkpoints": "Checkpoints", @@ -70,6 +71,53 @@ "slashCommands": { "description": "Manage your slash commands to quickly execute custom workflows and actions. Learn more" }, + "skills": { + "description": "Manage skills that provide contextual instructions to the agent. Skills are automatically applied when relevant to your tasks. Learn more", + "projectSkills": "Project Skills", + "globalSkills": "Global Skills", + "noProjectSkills": "No project skills configured. Create one to add project-specific agent capabilities.", + "noGlobalSkills": "No global skills configured. Create one to add agent capabilities available across all projects.", + "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.", + "confirm": "Delete", + "cancel": "Cancel" + }, + "createDialog": { + "title": "Create New Skill", + "description": "Define a new skill template that provides contextual instructions to the agent.", + "nameLabel": "Name", + "namePlaceholder": "my-skill-name", + "nameHint": "Lowercase letters, numbers, and hyphens only (1-64 characters)", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Describe when this skill should be used...", + "descriptionHint": "Explain what this skill does and when the agent should apply it (1-1024 characters)", + "sourceLabel": "Location", + "sourceHint": "Choose whether this skill is available globally or only in this project", + "modeLabel": "Mode (optional)", + "modePlaceholder": "Any mode", + "modeHint": "Restrict this skill to a specific mode", + "modeAny": "Any mode", + "create": "Create", + "cancel": "Cancel" + }, + "source": { + "global": "Global (available in all projects)", + "project": "Project (this workspace only)" + }, + "validation": { + "nameRequired": "Name is required", + "nameTooLong": "Name must be 64 characters or less", + "nameInvalid": "Name must be 1-64 lowercase letters, numbers, or hyphens", + "descriptionRequired": "Description is required", + "descriptionTooLong": "Description must be 1024 characters or less" + } + }, "ui": { "collapseThinking": { "label": "Collapse Thinking messages by default", @@ -347,11 +395,11 @@ "getAnthropicApiKey": "Get Anthropic API Key", "anthropicUseAuthToken": "Pass Anthropic API Key as Authorization header instead of X-Api-Key", "anthropic1MContextBetaLabel": "Enable 1M context window (Beta)", - "anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Enable 1M context window (Beta)", - "awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4", "vertex1MContextBetaLabel": "Enable 1M context window (Beta)", - "vertex1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Get Baseten API Key", "cerebrasApiKey": "Cerebras API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 03defa59d01..3391076f616 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Experimental", "language": "Idioma", - "about": "Acerca de Roo Code" + "about": "Acerca de Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Obtener clave API de Anthropic", "anthropicUseAuthToken": "Pasar la clave API de Anthropic como encabezado de autorización en lugar de X-Api-Key", "anthropic1MContextBetaLabel": "Habilitar ventana de contexto de 1M (Beta)", - "anthropic1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Habilitar ventana de contexto de 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4", "vertex1MContextBetaLabel": "Habilitar ventana de contexto de 1M (Beta)", - "vertex1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4", "basetenApiKey": "Clave API de Baseten", "getBasetenApiKey": "Obtener clave API de Baseten", "cerebrasApiKey": "Clave API de Cerebras", @@ -992,5 +993,52 @@ "label": "Requerir {{primaryMod}}+Enter para enviar mensajes", "description": "Cuando está activado, debes presionar {{primaryMod}}+Enter para enviar mensajes en lugar de solo Enter" } + }, + "skills": { + "description": "Gestiona skills que proporcionan instrucciones contextuales al agente. Las skills se aplican automáticamente cuando son relevantes para tus tareas. Más información", + "projectSkills": "Skills del Proyecto", + "globalSkills": "Skills Globales", + "noProjectSkills": "No hay skills de proyecto configuradas. Crea una para añadir capacidades específicas del proyecto al agente.", + "noGlobalSkills": "No hay skills globales configuradas. Crea una para añadir capacidades al agente disponibles en todos los proyectos.", + "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.", + "confirm": "Eliminar", + "cancel": "Cancelar" + }, + "createDialog": { + "title": "Crear Nueva Skill", + "description": "Define una nueva plantilla de skill que proporcione instrucciones contextuales al agente.", + "nameLabel": "Nombre", + "namePlaceholder": "mi-nombre-de-skill", + "nameHint": "Solo letras minúsculas, números y guiones (1-64 caracteres)", + "descriptionLabel": "Descripción", + "descriptionPlaceholder": "Describe cuándo debería usarse esta skill...", + "descriptionHint": "Explica qué hace esta skill y cuándo el agente debería aplicarla (1-1024 caracteres)", + "sourceLabel": "Ubicación", + "sourceHint": "Elige si esta skill está disponible globalmente o solo en este proyecto", + "modeLabel": "Modo (opcional)", + "modePlaceholder": "Cualquier modo", + "modeHint": "Restringe esta skill a un modo específico", + "modeAny": "Cualquier modo", + "create": "Crear", + "cancel": "Cancelar" + }, + "source": { + "global": "Global (disponible en todos los proyectos)", + "project": "Proyecto (solo este espacio de trabajo)" + }, + "validation": { + "nameRequired": "El nombre es obligatorio", + "nameTooLong": "El nombre debe tener como máximo 64 caracteres", + "nameInvalid": "El nombre debe tener entre 1 y 64 caracteres, solo letras minúsculas, números o guiones", + "descriptionRequired": "La descripción es obligatoria", + "descriptionTooLong": "La descripción debe tener como máximo 1024 caracteres" + } } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index a9d0b895a6a..402d701d029 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Expérimental", "language": "Langue", - "about": "À propos de Roo Code" + "about": "À propos de Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Obtenir la clé API Anthropic", "anthropicUseAuthToken": "Passer la clé API Anthropic comme en-tête d'autorisation au lieu de X-Api-Key", "anthropic1MContextBetaLabel": "Activer la fenêtre de contexte de 1M (Bêta)", - "anthropic1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Activer la fenêtre de contexte de 1M (Bêta)", - "awsBedrock1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4", "vertex1MContextBetaLabel": "Activer la fenêtre de contexte de 1M (Bêta)", - "vertex1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4", "basetenApiKey": "Clé API Baseten", "getBasetenApiKey": "Obtenir la clé API Baseten", "cerebrasApiKey": "Clé API Cerebras", @@ -992,5 +993,52 @@ "label": "Exiger {{primaryMod}}+Entrée pour envoyer les messages", "description": "Lorsqu'activé, tu dois appuyer sur {{primaryMod}}+Entrée pour envoyer des messages au lieu de simplement Entrée" } + }, + "skills": { + "description": "Gérez les skills qui fournissent des instructions contextuelles à l'agent. Les skills sont automatiquement appliquées lorsqu'elles sont pertinentes pour vos tâches. En savoir plus", + "projectSkills": "Skills du Projet", + "globalSkills": "Skills Globales", + "noProjectSkills": "Aucune skill de projet configurée. Créez-en une pour ajouter des capacités spécifiques au projet à l'agent.", + "noGlobalSkills": "Aucune skill globale configurée. Créez-en une pour ajouter des capacités à l'agent disponibles dans tous les projets.", + "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.", + "confirm": "Supprimer", + "cancel": "Annuler" + }, + "createDialog": { + "title": "Créer une Nouvelle Skill", + "description": "Définissez un nouveau modèle de skill qui fournit des instructions contextuelles à l'agent.", + "nameLabel": "Nom", + "namePlaceholder": "mon-nom-de-skill", + "nameHint": "Lettres minuscules, chiffres et tirets uniquement (1-64 caractères)", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Décrivez quand cette skill devrait être utilisée...", + "descriptionHint": "Expliquez ce que fait cette skill et quand l'agent devrait l'appliquer (1-1024 caractères)", + "sourceLabel": "Emplacement", + "sourceHint": "Choisissez si cette skill est disponible globalement ou uniquement dans ce projet", + "modeLabel": "Mode (optionnel)", + "modePlaceholder": "N'importe quel mode", + "modeHint": "Restreindre cette skill à un mode spécifique", + "modeAny": "N'importe quel mode", + "create": "Créer", + "cancel": "Annuler" + }, + "source": { + "global": "Globale (disponible dans tous les projets)", + "project": "Projet (cet espace de travail uniquement)" + }, + "validation": { + "nameRequired": "Le nom est obligatoire", + "nameTooLong": "Le nom doit contenir au maximum 64 caractères", + "nameInvalid": "Le nom doit contenir entre 1 et 64 lettres minuscules, chiffres ou tirets", + "descriptionRequired": "La description est obligatoire", + "descriptionTooLong": "La description doit contenir au maximum 1024 caractères" + } } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 967a00352fb..66289df2984 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "प्रायोगिक", "language": "भाषा", - "about": "परिचय" + "about": "परिचय", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Anthropic API कुंजी प्राप्त करें", "anthropicUseAuthToken": "X-Api-Key के बजाय Anthropic API कुंजी को Authorization हेडर के रूप में पास करें", "anthropic1MContextBetaLabel": "1M संदर्भ विंडो सक्षम करें (बीटा)", - "anthropic1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", + "anthropic1MContextBetaDescription": "Claude Sonnet 4 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", "awsBedrock1MContextBetaLabel": "1M संदर्भ विंडो सक्षम करें (बीटा)", - "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", + "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", "vertex1MContextBetaLabel": "1M संदर्भ विंडो सक्षम करें (बीटा)", - "vertex1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", + "vertex1MContextBetaDescription": "Claude Sonnet 4 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", "basetenApiKey": "Baseten API कुंजी", "getBasetenApiKey": "Baseten API कुंजी प्राप्त करें", "cerebrasApiKey": "Cerebras API कुंजी", @@ -993,5 +994,52 @@ "label": "संदेश भेजने के लिए {{primaryMod}}+Enter की आवश्यकता है", "description": "जब सक्षम हो, तो आपको केवल Enter के बजाय संदेश भेजने के लिए {{primaryMod}}+Enter दबाना होगा" } + }, + "skills": { + "description": "Skills का प्रबंधन करें जो एजेंट को संदर्भात्मक निर्देश प्रदान करते हैं। जब आपके कार्यों के लिए प्रासंगिक हों तो Skills स्वचालित रूप से लागू होते हैं। और जानें", + "projectSkills": "प्रोजेक्ट Skills", + "globalSkills": "ग्लोबल Skills", + "noProjectSkills": "कोई प्रोजेक्ट skills कॉन्फ़िगर नहीं किया गया। प्रोजेक्ट-विशिष्ट एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।", + "noGlobalSkills": "कोई ग्लोबल skills कॉन्फ़िगर नहीं किया गया। सभी प्रोजेक्ट्स में उपलब्ध एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।", + "addSkill": "Skill जोड़ें", + "editSkill": "Skill संपादित करें", + "deleteSkill": "Skill हटाएं", + "changeMode": "मोड बदलें", + "modeAny": "कोई भी मोड", + "deleteDialog": { + "title": "Skill हटाएं", + "description": "क्या आप वाकई skill \"{{name}}\" को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।", + "confirm": "हटाएं", + "cancel": "रद्द करें" + }, + "createDialog": { + "title": "नया Skill बनाएं", + "description": "एक नया skill टेम्पलेट परिभाषित करें जो एजेंट को संदर्भात्मक निर्देश प्रदान करता है।", + "nameLabel": "नाम", + "namePlaceholder": "my-skill-name", + "nameHint": "केवल छोटे अक्षर, संख्याएं और हाइफ़न (1-64 वर्ण)", + "descriptionLabel": "विवरण", + "descriptionPlaceholder": "वर्णन करें कि इस skill का उपयोग कब किया जाना चाहिए...", + "descriptionHint": "समझाएं कि यह skill क्या करता है और एजेंट को इसे कब लागू करना चाहिए (1-1024 वर्ण)", + "sourceLabel": "स्थान", + "sourceHint": "चुनें कि यह skill ग्लोबल रूप से उपलब्ध है या केवल इस प्रोजेक्ट में", + "modeLabel": "मोड (वैकल्पिक)", + "modePlaceholder": "कोई भी मोड", + "modeHint": "इस skill को किसी विशिष्ट मोड तक सीमित करें", + "modeAny": "कोई भी मोड", + "create": "बनाएं", + "cancel": "रद्द करें" + }, + "source": { + "global": "ग्लोबल (सभी प्रोजेक्ट्स में उपलब्ध)", + "project": "प्रोजेक्ट (केवल यह वर्कस्पेस)" + }, + "validation": { + "nameRequired": "नाम आवश्यक है", + "nameTooLong": "नाम 64 वर्णों से अधिक नहीं होना चाहिए", + "nameInvalid": "नाम 1-64 छोटे अक्षर, संख्याएं या हाइफ़न होना चाहिए", + "descriptionRequired": "विवरण आवश्यक है", + "descriptionTooLong": "विवरण 1024 वर्णों से अधिक नहीं होना चाहिए" + } } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index b117dd15bc7..e166cda621b 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Eksperimental", "language": "Bahasa", - "about": "Tentang Roo Code" + "about": "Tentang Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -342,11 +343,11 @@ "getAnthropicApiKey": "Dapatkan Anthropic API Key", "anthropicUseAuthToken": "Kirim Anthropic API Key sebagai Authorization header alih-alih X-Api-Key", "anthropic1MContextBetaLabel": "Aktifkan jendela konteks 1M (Beta)", - "anthropic1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Aktifkan jendela konteks 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4", "vertex1MContextBetaLabel": "Aktifkan jendela konteks 1M (Beta)", - "vertex1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Dapatkan Baseten API Key", "cerebrasApiKey": "Cerebras API Key", @@ -1022,5 +1023,52 @@ "label": "Memerlukan {{primaryMod}}+Enter untuk mengirim pesan", "description": "Ketika diaktifkan, kamu harus menekan {{primaryMod}}+Enter untuk mengirim pesan alih-alih hanya Enter" } + }, + "skills": { + "description": "Kelola skills yang memberikan instruksi kontekstual kepada agen. Skills diterapkan secara otomatis saat relevan dengan tugas Anda. Pelajari lebih lanjut", + "projectSkills": "Skills Proyek", + "globalSkills": "Skills Global", + "noProjectSkills": "Tidak ada skills proyek yang dikonfigurasi. Buat satu untuk menambahkan kemampuan agen khusus proyek.", + "noGlobalSkills": "Tidak ada skills global yang dikonfigurasi. Buat satu untuk menambahkan kemampuan agen yang tersedia di semua proyek.", + "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.", + "confirm": "Hapus", + "cancel": "Batal" + }, + "createDialog": { + "title": "Buat Skill Baru", + "description": "Tentukan template skill baru yang memberikan instruksi kontekstual kepada agen.", + "nameLabel": "Nama", + "namePlaceholder": "nama-skill-saya", + "nameHint": "Hanya huruf kecil, angka, dan tanda hubung (1-64 karakter)", + "descriptionLabel": "Deskripsi", + "descriptionPlaceholder": "Jelaskan kapan skill ini harus digunakan...", + "descriptionHint": "Jelaskan apa yang dilakukan skill ini dan kapan agen harus menerapkannya (1-1024 karakter)", + "sourceLabel": "Lokasi", + "sourceHint": "Pilih apakah skill ini tersedia secara global atau hanya di proyek ini", + "modeLabel": "Mode (opsional)", + "modePlaceholder": "Mode apa saja", + "modeHint": "Batasi skill ini ke mode tertentu", + "modeAny": "Mode apa saja", + "create": "Buat", + "cancel": "Batal" + }, + "source": { + "global": "Global (tersedia di semua proyek)", + "project": "Proyek (workspace ini saja)" + }, + "validation": { + "nameRequired": "Nama diperlukan", + "nameTooLong": "Nama harus 64 karakter atau kurang", + "nameInvalid": "Nama harus 1-64 huruf kecil, angka, atau tanda hubung", + "descriptionRequired": "Deskripsi diperlukan", + "descriptionTooLong": "Deskripsi harus 1024 karakter atau kurang" + } } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index ac35d794500..351db0a9cc7 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Sperimentale", "language": "Lingua", - "about": "Informazioni su Roo Code" + "about": "Informazioni su Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Ottieni chiave API Anthropic", "anthropicUseAuthToken": "Passa la chiave API Anthropic come header di autorizzazione invece di X-Api-Key", "anthropic1MContextBetaLabel": "Abilita finestra di contesto da 1M (Beta)", - "anthropic1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Abilita finestra di contesto da 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4", "vertex1MContextBetaLabel": "Abilita finestra di contesto da 1M (Beta)", - "vertex1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4", "basetenApiKey": "Chiave API Baseten", "getBasetenApiKey": "Ottieni chiave API Baseten", "cerebrasApiKey": "Chiave API Cerebras", @@ -993,5 +994,52 @@ "label": "Richiedi {{primaryMod}}+Invio per inviare messaggi", "description": "Quando abilitato, devi premere {{primaryMod}}+Invio per inviare messaggi invece di solo Invio" } + }, + "skills": { + "description": "Gestisci le skills che forniscono istruzioni contestuali all'agente. Le skills vengono applicate automaticamente quando rilevanti per le tue attività. Scopri di più", + "projectSkills": "Skills del Progetto", + "globalSkills": "Skills Globali", + "noProjectSkills": "Nessuna skill di progetto configurata. Creane una per aggiungere capacità specifiche del progetto all'agente.", + "noGlobalSkills": "Nessuna skill globale configurata. Creane una per aggiungere capacità all'agente disponibili in tutti i progetti.", + "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.", + "confirm": "Elimina", + "cancel": "Annulla" + }, + "createDialog": { + "title": "Crea Nuova Skill", + "description": "Definisci un nuovo modello di skill che fornisce istruzioni contestuali all'agente.", + "nameLabel": "Nome", + "namePlaceholder": "il-mio-nome-skill", + "nameHint": "Solo lettere minuscole, numeri e trattini (1-64 caratteri)", + "descriptionLabel": "Descrizione", + "descriptionPlaceholder": "Descrivi quando questa skill dovrebbe essere utilizzata...", + "descriptionHint": "Spiega cosa fa questa skill e quando l'agente dovrebbe applicarla (1-1024 caratteri)", + "sourceLabel": "Posizione", + "sourceHint": "Scegli se questa skill è disponibile globalmente o solo in questo progetto", + "modeLabel": "Modalità (opzionale)", + "modePlaceholder": "Qualsiasi modalità", + "modeHint": "Limita questa skill a una modalità specifica", + "modeAny": "Qualsiasi modalità", + "create": "Crea", + "cancel": "Annulla" + }, + "source": { + "global": "Globale (disponibile in tutti i progetti)", + "project": "Progetto (solo questo workspace)" + }, + "validation": { + "nameRequired": "Il nome è obbligatorio", + "nameTooLong": "Il nome deve essere di massimo 64 caratteri", + "nameInvalid": "Il nome deve contenere 1-64 lettere minuscole, numeri o trattini", + "descriptionRequired": "La descrizione è obbligatoria", + "descriptionTooLong": "La descrizione deve essere di massimo 1024 caratteri" + } } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d45b8ab401c..79ecee17e36 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "実験的", "language": "言語", - "about": "Roo Codeについて" + "about": "Roo Codeについて", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Anthropic APIキーを取得", "anthropicUseAuthToken": "Anthropic APIキーをX-Api-Keyの代わりにAuthorizationヘッダーとして渡す", "anthropic1MContextBetaLabel": "1Mコンテキストウィンドウを有効にする(ベータ版)", - "anthropic1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6のコンテキストウィンドウを100万トークンに拡張します", + "anthropic1MContextBetaDescription": "Claude Sonnet 4のコンテキストウィンドウを100万トークンに拡張します", "awsBedrock1MContextBetaLabel": "1Mコンテキストウィンドウを有効にする(ベータ版)", - "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6のコンテキストウィンドウを100万トークンに拡張します", + "awsBedrock1MContextBetaDescription": "Claude Sonnet 4のコンテキストウィンドウを100万トークンに拡張します", "vertex1MContextBetaLabel": "1Mコンテキストウィンドウを有効にする(ベータ版)", - "vertex1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6のコンテキストウィンドウを100万トークンに拡張します", + "vertex1MContextBetaDescription": "Claude Sonnet 4のコンテキストウィンドウを100万トークンに拡張します", "basetenApiKey": "Baseten APIキー", "getBasetenApiKey": "Baseten APIキーを取得", "cerebrasApiKey": "Cerebras APIキー", @@ -993,5 +994,52 @@ "label": "メッセージを送信するには{{primaryMod}}+Enterが必要", "description": "有効にすると、Enterだけでなく{{primaryMod}}+Enterを押してメッセージを送信する必要があります" } + }, + "skills": { + "description": "エージェントにコンテキスト指示を提供するスキルを管理します。スキルはタスクに関連する場合に自動的に適用されます。詳細を見る", + "projectSkills": "プロジェクトスキル", + "globalSkills": "グローバルスキル", + "noProjectSkills": "プロジェクトスキルが設定されていません。プロジェクト固有のエージェント機能を追加するには、作成してください。", + "noGlobalSkills": "グローバルスキルが設定されていません。すべてのプロジェクトで利用可能なエージェント機能を追加するには、作成してください。", + "addSkill": "スキルを追加", + "editSkill": "スキルを編集", + "deleteSkill": "スキルを削除", + "changeMode": "モードを変更", + "modeAny": "任意のモード", + "deleteDialog": { + "title": "スキルを削除", + "description": "スキル「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。", + "confirm": "削除", + "cancel": "キャンセル" + }, + "createDialog": { + "title": "新しいスキルを作成", + "description": "エージェントにコンテキスト指示を提供する新しいスキルテンプレートを定義します。", + "nameLabel": "名前", + "namePlaceholder": "my-skill-name", + "nameHint": "小文字、数字、ハイフンのみ(1〜64文字)", + "descriptionLabel": "説明", + "descriptionPlaceholder": "このスキルをいつ使用するか説明してください...", + "descriptionHint": "このスキルが何をするか、エージェントがいつ適用すべきかを説明してください(1〜1024文字)", + "sourceLabel": "場所", + "sourceHint": "このスキルがグローバルに利用可能か、このプロジェクトのみかを選択してください", + "modeLabel": "モード(オプション)", + "modePlaceholder": "全てのモード", + "modeHint": "このスキルを特定のモードに制限する", + "modeAny": "全てのモード", + "create": "作成", + "cancel": "キャンセル" + }, + "source": { + "global": "グローバル(すべてのプロジェクトで利用可能)", + "project": "プロジェクト(このワークスペースのみ)" + }, + "validation": { + "nameRequired": "名前は必須です", + "nameTooLong": "名前は64文字以内である必要があります", + "nameInvalid": "名前は1〜64文字の小文字、数字、またはハイフンである必要があります", + "descriptionRequired": "説明は必須です", + "descriptionTooLong": "説明は1024文字以内である必要があります" + } } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 36611e7557d..c044a22f22f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "실험적", "language": "언어", - "about": "Roo Code 정보" + "about": "Roo Code 정보", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Anthropic API 키 받기", "anthropicUseAuthToken": "X-Api-Key 대신 Authorization 헤더로 Anthropic API 키 전달", "anthropic1MContextBetaLabel": "1M 컨텍스트 창 활성화 (베타)", - "anthropic1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6의 컨텍스트 창을 100만 토큰으로 확장", + "anthropic1MContextBetaDescription": "Claude Sonnet 4의 컨텍스트 창을 100만 토큰으로 확장", "awsBedrock1MContextBetaLabel": "1M 컨텍스트 창 활성화 (베타)", - "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6의 컨텍스트 창을 100만 토큰으로 확장", + "awsBedrock1MContextBetaDescription": "Claude Sonnet 4의 컨텍스트 창을 100만 토큰으로 확장", "vertex1MContextBetaLabel": "1M 컨텍스트 창 활성화 (베타)", - "vertex1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6의 컨텍스트 창을 100만 토큰으로 확장", + "vertex1MContextBetaDescription": "Claude Sonnet 4의 컨텍스트 창을 100만 토큰으로 확장", "basetenApiKey": "Baseten API 키", "getBasetenApiKey": "Baseten API 키 가져오기", "cerebrasApiKey": "Cerebras API 키", @@ -993,5 +994,52 @@ "label": "메시지를 보내려면 {{primaryMod}}+Enter가 필요", "description": "활성화하면 Enter만으로는 안 되고 {{primaryMod}}+Enter를 눌러야 메시지를 보낼 수 있습니다" } + }, + "skills": { + "description": "에이전트에 컨텍스트 지침을 제공하는 스킬을 관리합니다. 스킬은 작업과 관련이 있을 때 자동으로 적용됩니다. 자세히 알아보기", + "projectSkills": "프로젝트 스킬", + "globalSkills": "전역 스킬", + "noProjectSkills": "구성된 프로젝트 스킬이 없습니다. 프로젝트별 에이전트 기능을 추가하려면 하나를 만드세요.", + "noGlobalSkills": "구성된 전역 스킬이 없습니다. 모든 프로젝트에서 사용할 수 있는 에이전트 기능을 추가하려면 하나를 만드세요.", + "addSkill": "스킬 추가", + "editSkill": "스킬 편집", + "deleteSkill": "스킬 삭제", + "changeMode": "모드 변경", + "modeAny": "모든 모드", + "deleteDialog": { + "title": "스킬 삭제", + "description": "스킬 \"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "confirm": "삭제", + "cancel": "취소" + }, + "createDialog": { + "title": "새 스킬 만들기", + "description": "에이전트에 컨텍스트 지침을 제공하는 새 스킬 템플릿을 정의합니다.", + "nameLabel": "이름", + "namePlaceholder": "my-skill-name", + "nameHint": "소문자, 숫자 및 하이픈만 사용(1-64자)", + "descriptionLabel": "설명", + "descriptionPlaceholder": "이 스킬을 언제 사용해야 하는지 설명하세요...", + "descriptionHint": "이 스킬이 무엇을 하는지, 에이전트가 언제 적용해야 하는지 설명하세요(1-1024자)", + "sourceLabel": "위치", + "sourceHint": "이 스킬을 전역으로 사용할지 이 프로젝트에만 사용할지 선택하세요", + "modeLabel": "모드 (선택사항)", + "modePlaceholder": "모든 모드", + "modeHint": "이 스킬을 특정 모드로 제한", + "modeAny": "모든 모드", + "create": "만들기", + "cancel": "취소" + }, + "source": { + "global": "전역 (모든 프로젝트에서 사용 가능)", + "project": "프로젝트 (이 작업공간만)" + }, + "validation": { + "nameRequired": "이름은 필수입니다", + "nameTooLong": "이름은 64자 이하여야 합니다", + "nameInvalid": "이름은 1-64자의 소문자, 숫자 또는 하이픈이어야 합니다", + "descriptionRequired": "설명은 필수입니다", + "descriptionTooLong": "설명은 1024자 이하여야 합니다" + } } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 7f4633569a8..f4cd21e66bb 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Experimenteel", "language": "Taal", - "about": "Over Roo Code" + "about": "Over Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Anthropic API-sleutel ophalen", "anthropicUseAuthToken": "Anthropic API-sleutel als Authorization-header doorgeven in plaats van X-Api-Key", "anthropic1MContextBetaLabel": "1M contextvenster inschakelen (bèta)", - "anthropic1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "1M contextvenster inschakelen (bèta)", - "awsBedrock1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4", "vertex1MContextBetaLabel": "1M contextvenster inschakelen (bèta)", - "vertex1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4", "basetenApiKey": "Baseten API-sleutel", "getBasetenApiKey": "Baseten API-sleutel verkrijgen", "cerebrasApiKey": "Cerebras API-sleutel", @@ -993,5 +994,52 @@ "label": "Vereist {{primaryMod}}+Enter om berichten te versturen", "description": "Wanneer ingeschakeld, moet je {{primaryMod}}+Enter indrukken om berichten te versturen in plaats van alleen Enter" } + }, + "skills": { + "description": "Beheer skills die contextuele instructies aan de agent verstrekken. Skills worden automatisch toegepast wanneer ze relevant zijn voor uw taken. Meer informatie", + "projectSkills": "Projectskills", + "globalSkills": "Globale Skills", + "noProjectSkills": "Geen projectskills geconfigureerd. Maak er een om projectspecifieke agentmogelijkheden toe te voegen.", + "noGlobalSkills": "Geen globale skills geconfigureerd. Maak er een om agentmogelijkheden toe te voegen die beschikbaar zijn in alle projecten.", + "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.", + "confirm": "Verwijderen", + "cancel": "Annuleren" + }, + "createDialog": { + "title": "Nieuwe Skill maken", + "description": "Definieer een nieuwe skillsjabloon die contextuele instructies aan de agent verstrekt.", + "nameLabel": "Naam", + "namePlaceholder": "mijn-skill-naam", + "nameHint": "Alleen kleine letters, cijfers en streepjes (1-64 tekens)", + "descriptionLabel": "Beschrijving", + "descriptionPlaceholder": "Beschrijf wanneer deze skill moet worden gebruikt...", + "descriptionHint": "Leg uit wat deze skill doet en wanneer de agent deze moet toepassen (1-1024 tekens)", + "sourceLabel": "Locatie", + "sourceHint": "Kies of deze skill globaal beschikbaar is of alleen in dit project", + "modeLabel": "Modus (optioneel)", + "modePlaceholder": "Elke modus", + "modeHint": "Beperk deze skill tot een specifieke modus", + "modeAny": "Elke modus", + "create": "Maken", + "cancel": "Annuleren" + }, + "source": { + "global": "Globaal (beschikbaar in alle projecten)", + "project": "Project (alleen deze workspace)" + }, + "validation": { + "nameRequired": "Naam is verplicht", + "nameTooLong": "Naam moet maximaal 64 tekens zijn", + "nameInvalid": "Naam moet 1-64 kleine letters, cijfers of streepjes zijn", + "descriptionRequired": "Beschrijving is verplicht", + "descriptionTooLong": "Beschrijving moet maximaal 1024 tekens zijn" + } } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bb69e9157ca..632f9811df1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Eksperymentalne", "language": "Język", - "about": "O Roo Code" + "about": "O Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Uzyskaj klucz API Anthropic", "anthropicUseAuthToken": "Przekaż klucz API Anthropic jako nagłówek Authorization zamiast X-Api-Key", "anthropic1MContextBetaLabel": "Włącz okno kontekstowe 1M (Beta)", - "anthropic1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Włącz okno kontekstowe 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4", "vertex1MContextBetaLabel": "Włącz okno kontekstowe 1M (Beta)", - "vertex1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4", "basetenApiKey": "Klucz API Baseten", "getBasetenApiKey": "Uzyskaj klucz API Baseten", "cerebrasApiKey": "Klucz API Cerebras", @@ -993,5 +994,52 @@ "label": "Wymagaj {{primaryMod}}+Enter do wysyłania wiadomości", "description": "Po włączeniu musisz nacisnąć {{primaryMod}}+Enter, aby wysłać wiadomości, zamiast tylko Enter" } + }, + "skills": { + "description": "Zarządzaj umiejętnościami, które dostarczają kontekstowe instrukcje dla agenta. Umiejętności są automatycznie stosowane, gdy są istotne dla Twoich zadań. Dowiedz się więcej", + "projectSkills": "Umiejętności Projektu", + "globalSkills": "Umiejętności Globalne", + "noProjectSkills": "Brak skonfigurowanych umiejętności projektu. Utwórz jedną, aby dodać możliwości agenta specyficzne dla projektu.", + "noGlobalSkills": "Brak skonfigurowanych umiejętności globalnych. Utwórz jedną, aby dodać możliwości agenta dostępne we wszystkich projektach.", + "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ąć.", + "confirm": "Usuń", + "cancel": "Anuluj" + }, + "createDialog": { + "title": "Utwórz Nową Umiejętność", + "description": "Zdefiniuj nowy szablon umiejętności, który dostarcza kontekstowe instrukcje dla agenta.", + "nameLabel": "Nazwa", + "namePlaceholder": "moja-nazwa-umiejetnosci", + "nameHint": "Tylko małe litery, cyfry i myślniki (1-64 znaki)", + "descriptionLabel": "Opis", + "descriptionPlaceholder": "Opisz, kiedy ta umiejętność powinna być użyta...", + "descriptionHint": "Wyjaśnij, co robi ta umiejętność i kiedy agent powinien ją zastosować (1-1024 znaki)", + "sourceLabel": "Lokalizacja", + "sourceHint": "Wybierz, czy ta umiejętność jest dostępna globalnie, czy tylko w tym projekcie", + "modeLabel": "Tryb (opcjonalnie)", + "modePlaceholder": "Dowolny tryb", + "modeHint": "Ogranicz tę umiejętność do określonego trybu", + "modeAny": "Dowolny tryb", + "create": "Utwórz", + "cancel": "Anuluj" + }, + "source": { + "global": "Globalnie (dostępne we wszystkich projektach)", + "project": "Projekt (tylko ten obszar roboczy)" + }, + "validation": { + "nameRequired": "Nazwa jest wymagana", + "nameTooLong": "Nazwa musi mieć maksymalnie 64 znaki", + "nameInvalid": "Nazwa musi zawierać 1-64 małe litery, cyfry lub myślniki", + "descriptionRequired": "Opis jest wymagany", + "descriptionTooLong": "Opis musi mieć maksymalnie 1024 znaki" + } } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 361b616806d..f30ac10bdb3 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Experimental", "language": "Idioma", - "about": "Sobre" + "about": "Sobre", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Obter chave de API Anthropic", "anthropicUseAuthToken": "Passar a chave de API Anthropic como cabeçalho Authorization em vez de X-Api-Key", "anthropic1MContextBetaLabel": "Ativar janela de contexto de 1M (Beta)", - "anthropic1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Ativar janela de contexto de 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4", "vertex1MContextBetaLabel": "Ativar janela de contexto de 1M (Beta)", - "vertex1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4", "basetenApiKey": "Chave de API Baseten", "getBasetenApiKey": "Obter chave de API Baseten", "cerebrasApiKey": "Chave de API Cerebras", @@ -993,5 +994,52 @@ "label": "Requer {{primaryMod}}+Enter para enviar mensagens", "description": "Quando ativado, você deve pressionar {{primaryMod}}+Enter para enviar mensagens em vez de apenas Enter" } + }, + "skills": { + "description": "Gerencie skills que fornecem instruções contextuais ao agente. As skills são aplicadas automaticamente quando relevantes para suas tarefas. Saiba mais", + "projectSkills": "Skills do Projeto", + "globalSkills": "Skills Globais", + "noProjectSkills": "Nenhuma skill de projeto configurada. Crie uma para adicionar capacidades específicas do projeto ao agente.", + "noGlobalSkills": "Nenhuma skill global configurada. Crie uma para adicionar capacidades ao agente disponíveis em todos os projetos.", + "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.", + "confirm": "Excluir", + "cancel": "Cancelar" + }, + "createDialog": { + "title": "Criar Nova Skill", + "description": "Defina um novo modelo de skill que fornece instruções contextuais ao agente.", + "nameLabel": "Nome", + "namePlaceholder": "meu-nome-de-skill", + "nameHint": "Apenas letras minúsculas, números e hífens (1-64 caracteres)", + "descriptionLabel": "Descrição", + "descriptionPlaceholder": "Descreva quando esta skill deve ser usada...", + "descriptionHint": "Explique o que esta skill faz e quando o agente deve aplicá-la (1-1024 caracteres)", + "sourceLabel": "Localização", + "sourceHint": "Escolha se esta skill está disponível globalmente ou apenas neste projeto", + "modeLabel": "Modo (opcional)", + "modePlaceholder": "Qualquer modo", + "modeHint": "Restrinja esta skill a um modo específico", + "modeAny": "Qualquer modo", + "create": "Criar", + "cancel": "Cancelar" + }, + "source": { + "global": "Global (disponível em todos os projetos)", + "project": "Projeto (apenas este workspace)" + }, + "validation": { + "nameRequired": "O nome é obrigatório", + "nameTooLong": "O nome deve ter no máximo 64 caracteres", + "nameInvalid": "O nome deve ter 1-64 letras minúsculas, números ou hífens", + "descriptionRequired": "A descrição é obrigatória", + "descriptionTooLong": "A descrição deve ter no máximo 1024 caracteres" + } } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 7f44785806e..98b9d965652 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Экспериментальное", "language": "Язык", - "about": "О Roo Code" + "about": "О Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Получить Anthropic API-ключ", "anthropicUseAuthToken": "Передавать Anthropic API-ключ как Authorization-заголовок вместо X-Api-Key", "anthropic1MContextBetaLabel": "Включить контекстное окно 1M (бета)", - "anthropic1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Включить контекстное окно 1M (бета)", - "awsBedrock1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4", "vertex1MContextBetaLabel": "Включить контекстное окно 1M (бета)", - "vertex1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4", "basetenApiKey": "Baseten API-ключ", "getBasetenApiKey": "Получить Baseten API-ключ", "cerebrasApiKey": "Cerebras API-ключ", @@ -993,5 +994,52 @@ "label": "Требовать {{primaryMod}}+Enter для отправки сообщений", "description": "Если включено, необходимо нажать {{primaryMod}}+Enter для отправки сообщений вместо простого Enter" } + }, + "skills": { + "description": "Управляйте навыками, которые предоставляют контекстные инструкции агенту. Навыки автоматически применяются, когда они релевантны вашим задачам. Узнать больше", + "projectSkills": "Навыки Проекта", + "globalSkills": "Глобальные Навыки", + "noProjectSkills": "Навыки проекта не настроены. Создайте навык, чтобы добавить возможности агента для конкретного проекта.", + "noGlobalSkills": "Глобальные навыки не настроены. Создайте навык, чтобы добавить возможности агента, доступные во всех проектах.", + "addSkill": "Добавить Навык", + "editSkill": "Редактировать навык", + "deleteSkill": "Удалить навык", + "changeMode": "Изменить режим", + "modeAny": "Любой режим", + "deleteDialog": { + "title": "Удалить Навык", + "description": "Вы уверены, что хотите удалить навык \"{{name}}\"? Это действие нельзя отменить.", + "confirm": "Удалить", + "cancel": "Отмена" + }, + "createDialog": { + "title": "Создать Новый Навык", + "description": "Определите новый шаблон навыка, который предоставляет контекстные инструкции агенту.", + "nameLabel": "Имя", + "namePlaceholder": "my-skill-name", + "nameHint": "Только строчные буквы, цифры и дефисы (1-64 символа)", + "descriptionLabel": "Описание", + "descriptionPlaceholder": "Опишите, когда следует использовать этот навык...", + "descriptionHint": "Объясните, что делает этот навык и когда агент должен его применять (1-1024 символа)", + "sourceLabel": "Расположение", + "sourceHint": "Выберите, доступен ли этот навык глобально или только в этом проекте", + "modeLabel": "Режим (необязательно)", + "modePlaceholder": "Любой режим", + "modeHint": "Ограничьте этот навык определенным режимом", + "modeAny": "Любой режим", + "create": "Создать", + "cancel": "Отмена" + }, + "source": { + "global": "Глобальный (доступен во всех проектах)", + "project": "Проект (только эта рабочая область)" + }, + "validation": { + "nameRequired": "Имя обязательно", + "nameTooLong": "Имя должно быть не более 64 символов", + "nameInvalid": "Имя должно содержать 1-64 строчные буквы, цифры или дефисы", + "descriptionRequired": "Описание обязательно", + "descriptionTooLong": "Описание должно быть не более 1024 символов" + } } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 1eff6525ad4..872f2ea3c73 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Deneysel", "language": "Dil", - "about": "Roo Code Hakkında" + "about": "Roo Code Hakkında", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Anthropic API Anahtarı Al", "anthropicUseAuthToken": "Anthropic API Anahtarını X-Api-Key yerine Authorization başlığı olarak geçir", "anthropic1MContextBetaLabel": "1M bağlam penceresini etkinleştir (Beta)", - "anthropic1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 için bağlam penceresini 1 milyon token'a genişletir", + "anthropic1MContextBetaDescription": "Claude Sonnet 4 için bağlam penceresini 1 milyon token'a genişletir", "awsBedrock1MContextBetaLabel": "1M bağlam penceresini etkinleştir (Beta)", - "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 için bağlam penceresini 1 milyon token'a genişletir", + "awsBedrock1MContextBetaDescription": "Claude Sonnet 4 için bağlam penceresini 1 milyon token'a genişletir", "vertex1MContextBetaLabel": "1M bağlam penceresini etkinleştir (Beta)", - "vertex1MContextBetaDescription": "Claude Sonnet 4 / 4.5 / Claude Opus 4.6 için bağlam penceresini 1 milyon token'a genişletir", + "vertex1MContextBetaDescription": "Claude Sonnet 4 için bağlam penceresini 1 milyon token'a genişletir", "basetenApiKey": "Baseten API Anahtarı", "getBasetenApiKey": "Baseten API Anahtarı Al", "cerebrasApiKey": "Cerebras API Anahtarı", @@ -993,5 +994,52 @@ "label": "Mesaj göndermek için {{primaryMod}}+Enter gerekli", "description": "Etkinleştirildiğinde, sadece Enter yerine mesaj göndermek için {{primaryMod}}+Enter'a basmalısınız" } + }, + "skills": { + "description": "Ajana bağlamsal talimatlar sağlayan becerileri yönetin. Beceriler, görevlerinizle ilgili olduklarında otomatik olarak uygulanır. Daha fazla bilgi", + "projectSkills": "Proje Becerileri", + "globalSkills": "Genel Beceriler", + "noProjectSkills": "Yapılandırılmış proje becerisi yok. Projeye özgü ajan yetenekleri eklemek için bir tane oluşturun.", + "noGlobalSkills": "Yapılandırılmış genel beceri yok. Tüm projelerde kullanılabilir ajan yetenekleri eklemek için bir tane oluşturun.", + "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.", + "confirm": "Sil", + "cancel": "İptal" + }, + "createDialog": { + "title": "Yeni Beceri Oluştur", + "description": "Ajana bağlamsal talimatlar sağlayan yeni bir beceri şablonu tanımlayın.", + "nameLabel": "Ad", + "namePlaceholder": "benim-beceri-adim", + "nameHint": "Yalnızca küçük harfler, rakamlar ve kısa çizgiler (1-64 karakter)", + "descriptionLabel": "Açıklama", + "descriptionPlaceholder": "Bu becerinin ne zaman kullanılması gerektiğini açıklayın...", + "descriptionHint": "Bu becerinin ne yaptığını ve ajanın ne zaman uygulaması gerektiğini açıklayın (1-1024 karakter)", + "sourceLabel": "Konum", + "sourceHint": "Bu becerinin genel olarak mı yoksa yalnızca bu projede mi kullanılabilir olduğunu seçin", + "modeLabel": "Mod (isteğe bağlı)", + "modePlaceholder": "Herhangi bir mod", + "modeHint": "Bu beceriyi belirli bir modla sınırlayın", + "modeAny": "Herhangi bir mod", + "create": "Oluştur", + "cancel": "İptal" + }, + "source": { + "global": "Genel (tüm projelerde kullanılabilir)", + "project": "Proje (yalnızca bu çalışma alanı)" + }, + "validation": { + "nameRequired": "Ad gereklidir", + "nameTooLong": "Ad en fazla 64 karakter olmalıdır", + "nameInvalid": "Ad 1-64 küçük harf, rakam veya kısa çizgi olmalıdır", + "descriptionRequired": "Açıklama gereklidir", + "descriptionTooLong": "Açıklama en fazla 1024 karakter olmalıdır" + } } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 6e9a1ba7714..1eced935c26 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "Thử nghiệm", "language": "Ngôn ngữ", - "about": "Giới thiệu" + "about": "Giới thiệu", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "Lấy khóa API Anthropic", "anthropicUseAuthToken": "Truyền khóa API Anthropic dưới dạng tiêu đề Authorization thay vì X-Api-Key", "anthropic1MContextBetaLabel": "Bật cửa sổ ngữ cảnh 1M (Beta)", - "anthropic1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "anthropic1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4", "awsBedrock1MContextBetaLabel": "Bật cửa sổ ngữ cảnh 1M (Beta)", - "awsBedrock1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "awsBedrock1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4", "vertex1MContextBetaLabel": "Bật cửa sổ ngữ cảnh 1M (Beta)", - "vertex1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4 / 4.5 / Claude Opus 4.6", + "vertex1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4", "basetenApiKey": "Khóa API Baseten", "getBasetenApiKey": "Lấy khóa API Baseten", "cerebrasApiKey": "Khóa API Cerebras", @@ -993,5 +994,52 @@ "label": "Yêu cầu {{primaryMod}}+Enter để gửi tin nhắn", "description": "Khi được bật, bạn phải nhấn {{primaryMod}}+Enter để gửi tin nhắn thay vì chỉ nhấn Enter" } + }, + "skills": { + "description": "Quản lý các skill cung cấp hướng dẫn theo ngữ cảnh cho agent. Các skill được áp dụng tự động khi chúng liên quan đến nhiệm vụ của bạn. Tìm hiểu thêm", + "projectSkills": "Skills Dự Án", + "globalSkills": "Skills Toàn Cục", + "noProjectSkills": "Không có skill dự án nào được cấu hình. Tạo một skill để thêm khả năng agent cụ thể cho dự án.", + "noGlobalSkills": "Không có skill toàn cục nào được cấu hình. Tạo một skill để thêm khả năng agent có sẵn trong tất cả các dự án.", + "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.", + "confirm": "Xóa", + "cancel": "Hủy" + }, + "createDialog": { + "title": "Tạo Skill Mới", + "description": "Xác định một mẫu skill mới cung cấp hướng dẫn theo ngữ cảnh cho agent.", + "nameLabel": "Tên", + "namePlaceholder": "ten-skill-cua-toi", + "nameHint": "Chỉ chữ thường, số và dấu gạch ngang (1-64 ký tự)", + "descriptionLabel": "Mô tả", + "descriptionPlaceholder": "Mô tả khi nào nên sử dụng skill này...", + "descriptionHint": "Giải thích skill này làm gì và khi nào agent nên áp dụng nó (1-1024 ký tự)", + "sourceLabel": "Vị trí", + "sourceHint": "Chọn xem skill này có sẵn toàn cục hay chỉ trong dự án này", + "modeLabel": "Chế độ (tùy chọn)", + "modePlaceholder": "Bất kỳ chế độ nào", + "modeHint": "Hạn chế skill này cho một chế độ cụ thể", + "modeAny": "Bất kỳ chế độ nào", + "create": "Tạo", + "cancel": "Hủy" + }, + "source": { + "global": "Toàn cục (có sẵn trong tất cả các dự án)", + "project": "Dự án (chỉ workspace này)" + }, + "validation": { + "nameRequired": "Tên là bắt buộc", + "nameTooLong": "Tên phải có tối đa 64 ký tự", + "nameInvalid": "Tên phải là 1-64 chữ thường, số hoặc dấu gạch ngang", + "descriptionRequired": "Mô tả là bắt buộc", + "descriptionTooLong": "Mô tả phải có tối đa 1024 ký tự" + } } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index f361f901e40..b70398c61ea 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "实验性", "language": "语言", - "about": "关于 Roo Code" + "about": "关于 Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -338,11 +339,11 @@ "getAnthropicApiKey": "获取 Anthropic API 密钥", "anthropicUseAuthToken": "将 Anthropic API 密钥作为 Authorization 标头传递,而不是 X-Api-Key", "anthropic1MContextBetaLabel": "启用 1M 上下文窗口 (Beta)", - "anthropic1MContextBetaDescription": "为 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 将上下文窗口扩展至 100 万个 token", + "anthropic1MContextBetaDescription": "为 Claude Sonnet 4 将上下文窗口扩展至 100 万个 token", "awsBedrock1MContextBetaLabel": "启用 1M 上下文窗口 (Beta)", - "awsBedrock1MContextBetaDescription": "为 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 将上下文窗口扩展至 100 万个 token", + "awsBedrock1MContextBetaDescription": "为 Claude Sonnet 4 将上下文窗口扩展至 100 万个 token", "vertex1MContextBetaLabel": "启用 1M 上下文窗口 (Beta)", - "vertex1MContextBetaDescription": "为 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 将上下文窗口扩展至 100 万个 token", + "vertex1MContextBetaDescription": "为 Claude Sonnet 4 将上下文窗口扩展至 100 万个 token", "basetenApiKey": "Baseten API 密钥", "getBasetenApiKey": "获取 Baseten API 密钥", "cerebrasApiKey": "Cerebras API 密钥", @@ -993,5 +994,52 @@ "label": "需要 {{primaryMod}}+Enter 发送消息", "description": "启用后,必须按 {{primaryMod}}+Enter 发送消息,而不仅仅是 Enter" } + }, + "skills": { + "description": "管理为代理提供上下文指令的技能。技能会在与您的任务相关时自动应用。了解更多", + "projectSkills": "项目技能", + "globalSkills": "全局技能", + "noProjectSkills": "未配置项目技能。创建一个以添加特定于项目的代理功能。", + "noGlobalSkills": "未配置全局技能。创建一个以添加在所有项目中可用的代理功能。", + "addSkill": "添加技能", + "editSkill": "编辑技能", + "deleteSkill": "删除技能", + "changeMode": "更改模式", + "modeAny": "任意模式", + "deleteDialog": { + "title": "删除技能", + "description": "您确定要删除技能\"{{name}}\"吗?此操作无法撤销。", + "confirm": "删除", + "cancel": "取消" + }, + "createDialog": { + "title": "创建新技能", + "description": "定义一个新的技能模板,为代理提供上下文指令。", + "nameLabel": "名称", + "namePlaceholder": "my-skill-name", + "nameHint": "仅小写字母、数字和连字符(1-64个字符)", + "descriptionLabel": "描述", + "descriptionPlaceholder": "描述何时应使用此技能...", + "descriptionHint": "解释此技能的作用以及代理应何时应用它(1-1024个字符)", + "sourceLabel": "位置", + "sourceHint": "选择此技能是全局可用还是仅在此项目中可用", + "modeLabel": "模式(可选)", + "modePlaceholder": "任何模式", + "modeHint": "将此技能限制为特定模式", + "modeAny": "任何模式", + "create": "创建", + "cancel": "取消" + }, + "source": { + "global": "全局(在所有项目中可用)", + "project": "项目(仅此工作区)" + }, + "validation": { + "nameRequired": "名称为必填项", + "nameTooLong": "名称不得超过64个字符", + "nameInvalid": "名称必须为1-64个小写字母、数字或连字符", + "descriptionRequired": "描述为必填项", + "descriptionTooLong": "描述不得超过1024个字符" + } } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 394019df091..506408ca73b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -41,7 +41,8 @@ "ui": "UI", "experimental": "實驗性", "language": "語言", - "about": "關於 Roo Code" + "about": "關於 Roo Code", + "skills": "Skills" }, "about": { "bugReport": { @@ -347,11 +348,11 @@ "getAnthropicApiKey": "取得 Anthropic API 金鑰", "anthropicUseAuthToken": "將 Anthropic API 金鑰作為 Authorization 標頭傳遞,而非使用 X-Api-Key", "anthropic1MContextBetaLabel": "啟用 1M 上下文視窗 (Beta)", - "anthropic1MContextBetaDescription": "為 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 將上下文視窗擴展至 100 萬個 token", + "anthropic1MContextBetaDescription": "為 Claude Sonnet 4 將上下文視窗擴展至 100 萬個 token", "awsBedrock1MContextBetaLabel": "啟用 1M 上下文視窗 (Beta)", - "awsBedrock1MContextBetaDescription": "為 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 將上下文視窗擴展至 100 萬個 token", + "awsBedrock1MContextBetaDescription": "為 Claude Sonnet 4 將上下文視窗擴展至 100 萬個 token", "vertex1MContextBetaLabel": "啟用 1M 上下文視窗 (Beta)", - "vertex1MContextBetaDescription": "為 Claude Sonnet 4 / 4.5 / Claude Opus 4.6 將上下文視窗擴展至 100 萬個 token", + "vertex1MContextBetaDescription": "為 Claude Sonnet 4 將上下文視窗擴展至 100 萬個 token", "basetenApiKey": "Baseten API 金鑰", "getBasetenApiKey": "取得 Baseten API 金鑰", "cerebrasApiKey": "Cerebras API 金鑰", @@ -990,5 +991,62 @@ "output": "輸出", "cacheReads": "快取讀取" } + }, + "ui": { + "collapseThinking": { + "label": "預設折疊「思考」訊息", + "description": "啟用後,「思考」塊將預設折疊,直到您與其互動" + }, + "requireCtrlEnterToSend": { + "label": "需要 {{primaryMod}}+Enter 傳送訊息", + "description": "啟用後,必須按 {{primaryMod}}+Enter 傳送訊息,而不只是 Enter" + } + }, + "skills": { + "description": "管理為代理提供上下文指令的技能。技能會在與您的任務相關時自動套用。深入了解", + "projectSkills": "專案技能", + "globalSkills": "全域技能", + "noProjectSkills": "未設定專案技能。建立一個以新增專案特定的代理功能。", + "noGlobalSkills": "未設定全域技能。建立一個以新增在所有專案中可用的代理功能。", + "addSkill": "新增技能", + "editSkill": "編輯技能", + "deleteSkill": "刪除技能", + "changeMode": "變更模式", + "modeAny": "任意模式", + "deleteDialog": { + "title": "刪除技能", + "description": "您確定要刪除技能「{{name}}」嗎?此動作無法復原。", + "confirm": "刪除", + "cancel": "取消" + }, + "createDialog": { + "title": "建立新技能", + "description": "定義新的技能範本,為代理提供上下文指令。", + "nameLabel": "名稱", + "namePlaceholder": "my-skill-name", + "nameHint": "僅限小寫字母、數字和連字號(1-64個字元)", + "descriptionLabel": "說明", + "descriptionPlaceholder": "描述何時應使用此技能...", + "descriptionHint": "說明此技能的作用以及代理應何時套用它(1-1024個字元)", + "sourceLabel": "位置", + "sourceHint": "選擇此技能是全域可用還是僅在此專案中可用", + "modeLabel": "模式(選填)", + "modePlaceholder": "任何模式", + "modeHint": "將此技能限制為特定模式", + "modeAny": "任何模式", + "create": "建立", + "cancel": "取消" + }, + "source": { + "global": "全域(在所有專案中可用)", + "project": "專案(僅此工作區)" + }, + "validation": { + "nameRequired": "名稱為必填", + "nameTooLong": "名稱不得超過64個字元", + "nameInvalid": "名稱必須為1-64個小寫字母、數字或連字號", + "descriptionRequired": "說明為必填", + "descriptionTooLong": "說明不得超過1024個字元" + } } } From ec8e86d82ea22b3302256bade15ca7e30cbf2292 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 3 Feb 2026 17:11:58 +0000 Subject: [PATCH 02/15] 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 --- packages/types/src/skills.ts | 81 +++ packages/types/src/vscode-extension-host.ts | 7 + .../__tests__/skillsMessageHandler.spec.ts | 14 +- src/core/webview/skillsMessageHandler.ts | 52 +- src/core/webview/webviewMessageHandler.ts | 5 + src/i18n/locales/ca/skills.json | 1 + src/i18n/locales/de/skills.json | 1 + src/i18n/locales/en/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 + src/services/skills/SkillsManager.ts | 135 ++++- .../skills/__tests__/SkillsManager.spec.ts | 12 +- src/shared/skills.ts | 12 +- .../src/components/chat/SlashCommandItem.tsx | 84 --- .../components/settings/CreateSkillDialog.tsx | 289 ++++++++++ .../settings/CreateSlashCommandDialog.tsx | 156 ++++++ .../src/components/settings/SettingsView.tsx | 6 +- .../components/settings/SkillsSettings.tsx | 393 +++++++++++++ .../settings/SlashCommandsSettings.tsx | 323 +++++------ .../__tests__/CreateSkillDialog.spec.tsx | 516 ++++++++++++++++++ .../SettingsView.change-detection.spec.tsx | 31 ++ .../settings/__tests__/SettingsView.spec.tsx | 20 + .../SettingsView.unsaved-changes.spec.tsx | 31 ++ .../__tests__/SkillsSettings.spec.tsx | 450 +++++++++++++++ .../__tests__/SlashCommandsSettings.spec.tsx | 445 ++++++--------- webview-ui/src/components/ui/checkbox.tsx | 2 +- webview-ui/src/components/ui/input.tsx | 2 +- webview-ui/src/components/ui/textarea.tsx | 2 +- webview-ui/src/i18n/locales/ca/settings.json | 62 ++- webview-ui/src/i18n/locales/de/settings.json | 80 ++- webview-ui/src/i18n/locales/en/settings.json | 66 ++- webview-ui/src/i18n/locales/es/settings.json | 64 ++- webview-ui/src/i18n/locales/fr/settings.json | 58 +- webview-ui/src/i18n/locales/hi/settings.json | 61 ++- webview-ui/src/i18n/locales/id/settings.json | 90 +-- webview-ui/src/i18n/locales/it/settings.json | 59 +- webview-ui/src/i18n/locales/ja/settings.json | 59 +- webview-ui/src/i18n/locales/ko/settings.json | 59 +- webview-ui/src/i18n/locales/nl/settings.json | 63 ++- webview-ui/src/i18n/locales/pl/settings.json | 59 +- .../src/i18n/locales/pt-BR/settings.json | 59 +- webview-ui/src/i18n/locales/ru/settings.json | 59 +- webview-ui/src/i18n/locales/tr/settings.json | 59 +- webview-ui/src/i18n/locales/vi/settings.json | 59 +- .../src/i18n/locales/zh-CN/settings.json | 59 +- .../src/i18n/locales/zh-TW/settings.json | 66 ++- 59 files changed, 3389 insertions(+), 838 deletions(-) create mode 100644 packages/types/src/skills.ts delete mode 100644 webview-ui/src/components/chat/SlashCommandItem.tsx create mode 100644 webview-ui/src/components/settings/CreateSkillDialog.tsx create mode 100644 webview-ui/src/components/settings/CreateSlashCommandDialog.tsx create mode 100644 webview-ui/src/components/settings/SkillsSettings.tsx create mode 100644 webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx diff --git a/packages/types/src/skills.ts b/packages/types/src/skills.ts new file mode 100644 index 00000000000..3e856612bcd --- /dev/null +++ b/packages/types/src/skills.ts @@ -0,0 +1,81 @@ +/** + * Skill metadata for discovery (loaded at startup) + * Only name and description are required for now + */ +export interface SkillMetadata { + name: string // Required: skill identifier + description: string // Required: when to use this skill + path: string // Absolute path to SKILL.md (or "" for built-in skills) + source: "global" | "project" | "built-in" // Where the skill was discovered + /** + * @deprecated Use modeSlugs instead. Kept for backward compatibility. + * If set, skill is only available in this mode. + */ + mode?: string + /** + * Mode slugs where this skill is available. + * - undefined or empty array means the skill is available in all modes ("Any mode"). + * - An array with one or more mode slugs restricts the skill to those modes. + */ + modeSlugs?: string[] +} + +/** + * Skill name validation constants per agentskills.io specification: + * https://agentskills.io/specification + * + * Name constraints: + * - 1-64 characters + * - Lowercase letters, numbers, and hyphens only + * - Must not start or end with a hyphen + * - Must not contain consecutive hyphens + */ +export const SKILL_NAME_MIN_LENGTH = 1 +export const SKILL_NAME_MAX_LENGTH = 64 + +/** + * Regex pattern for valid skill names. + * Matches: lowercase letters/numbers, optionally followed by groups of hyphen + lowercase letters/numbers. + * This ensures no leading/trailing hyphens and no consecutive hyphens. + */ +export const SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + +/** + * Error codes for skill name validation. + * These can be mapped to translation keys in the frontend or error messages in the backend. + */ +export enum SkillNameValidationError { + Empty = "empty", + TooLong = "too_long", + InvalidFormat = "invalid_format", +} + +/** + * Result of skill name validation. + */ +export interface SkillNameValidationResult { + valid: boolean + error?: SkillNameValidationError +} + +/** + * Validate a skill name according to agentskills.io specification. + * + * @param name - The skill name to validate + * @returns Validation result with error code if invalid + */ +export function validateSkillName(name: string): SkillNameValidationResult { + if (!name || name.length < SKILL_NAME_MIN_LENGTH) { + return { valid: false, error: SkillNameValidationError.Empty } + } + + if (name.length > SKILL_NAME_MAX_LENGTH) { + return { valid: false, error: SkillNameValidationError.TooLong } + } + + if (!SKILL_NAME_REGEX.test(name)) { + return { valid: false, error: SkillNameValidationError.InvalidFormat } + } + + return { valid: true } +} diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 0b7a28efc2d..13c1d8dbf0b 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -613,6 +613,7 @@ export interface WebviewMessage { | "createSkill" | "deleteSkill" | "moveSkill" + | "updateSkillModes" | "openSkillFile" text?: string editedMessageContent?: string @@ -649,9 +650,15 @@ export interface WebviewMessage { payload?: WebViewMessagePayload source?: "global" | "project" | "built-in" skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile) + /** @deprecated Use skillModeSlugs instead */ skillMode?: string // For skill operations (current mode restriction) + /** @deprecated Use newSkillModeSlugs instead */ newSkillMode?: string // For moveSkill (target mode) skillDescription?: string // For createSkill (skill description) + /** Mode slugs for skill operations. undefined/empty = any mode */ + skillModeSlugs?: string[] // For skill operations (mode restrictions) + /** Target mode slugs for updateSkillModes */ + newSkillModeSlugs?: string[] // For updateSkillModes (new mode restrictions) requestId?: string ids?: string[] terminalOperation?: "continue" | "abort" diff --git a/src/core/webview/__tests__/skillsMessageHandler.spec.ts b/src/core/webview/__tests__/skillsMessageHandler.spec.ts index f26194ee812..cdc571282f2 100644 --- a/src/core/webview/__tests__/skillsMessageHandler.spec.ts +++ b/src/core/webview/__tests__/skillsMessageHandler.spec.ts @@ -52,6 +52,7 @@ describe("skillsMessageHandler", () => { const mockDeleteSkill = vi.fn() const mockMoveSkill = vi.fn() const mockGetSkill = vi.fn() + const mockFindSkillByNameAndSource = vi.fn() const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => { const skillsManager = hasSkillsManager @@ -61,6 +62,7 @@ describe("skillsMessageHandler", () => { deleteSkill: mockDeleteSkill, moveSkill: mockMoveSkill, getSkill: mockGetSkill, + findSkillByNameAndSource: mockFindSkillByNameAndSource, } : undefined @@ -158,7 +160,7 @@ describe("skillsMessageHandler", () => { } as WebviewMessage) expect(result).toEqual(mockSkills) - expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", "code") + expect(mockCreateSkill).toHaveBeenCalledWith("new-skill", "project", "New skill description", ["code"]) }) it("returns undefined when required fields are missing", async () => { @@ -355,7 +357,7 @@ describe("skillsMessageHandler", () => { describe("handleOpenSkillFile", () => { it("opens a skill file successfully", async () => { const provider = createMockProvider(true) - mockGetSkill.mockReturnValue(mockSkills[0]) + mockFindSkillByNameAndSource.mockReturnValue(mockSkills[0]) await handleOpenSkillFile(provider, { type: "openSkillFile", @@ -363,13 +365,13 @@ describe("skillsMessageHandler", () => { source: "global", } as WebviewMessage) - expect(mockGetSkill).toHaveBeenCalledWith("test-skill", "global", undefined) + expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("test-skill", "global") expect(openFile).toHaveBeenCalledWith("/path/to/test-skill/SKILL.md") }) it("opens a skill file with mode restriction", async () => { const provider = createMockProvider(true) - mockGetSkill.mockReturnValue(mockSkills[1]) + mockFindSkillByNameAndSource.mockReturnValue(mockSkills[1]) await handleOpenSkillFile(provider, { type: "openSkillFile", @@ -378,7 +380,7 @@ describe("skillsMessageHandler", () => { skillMode: "code", } as WebviewMessage) - expect(mockGetSkill).toHaveBeenCalledWith("project-skill", "project", "code") + expect(mockFindSkillByNameAndSource).toHaveBeenCalledWith("project-skill", "project") expect(openFile).toHaveBeenCalledWith("/project/.roo/skills/project-skill/SKILL.md") }) @@ -416,7 +418,7 @@ describe("skillsMessageHandler", () => { it("shows error when skill is not found", async () => { const provider = createMockProvider(true) - mockGetSkill.mockReturnValue(undefined) + mockFindSkillByNameAndSource.mockReturnValue(undefined) await handleOpenSkillFile(provider, { type: "openSkillFile", diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index f09f22f58c5..f5db0473fb2 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -38,7 +38,8 @@ export async function handleCreateSkill( const skillName = message.skillName const source = message.source const skillDescription = message.skillDescription - const skillMode = message.skillMode + // Support new modeSlugs array or fall back to legacy skillMode + const modeSlugs = message.skillModeSlugs ?? (message.skillMode ? [message.skillMode] : undefined) if (!skillName || !source || !skillDescription) { throw new Error(t("skills:errors.missing_create_fields")) @@ -54,7 +55,7 @@ export async function handleCreateSkill( throw new Error(t("skills:errors.manager_unavailable")) } - const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, skillMode) + const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, modeSlugs) // Open the created file in the editor openFile(createdPath) @@ -81,7 +82,8 @@ export async function handleDeleteSkill( try { const skillName = message.skillName const source = message.source - const skillMode = message.skillMode + // Support new skillModeSlugs array or fall back to legacy skillMode + const skillMode = message.skillModeSlugs?.[0] ?? message.skillMode if (!skillName || !source) { throw new Error(t("skills:errors.missing_delete_fields")) @@ -152,6 +154,46 @@ export async function handleMoveSkill( } } +/** + * Handles the updateSkillModes message - updates the mode associations for a skill + */ +export async function handleUpdateSkillModes( + provider: ClineProvider, + message: WebviewMessage, +): Promise { + try { + const skillName = message.skillName + const source = message.source + const newModeSlugs = message.newSkillModeSlugs + + if (!skillName || !source) { + throw new Error(t("skills:errors.missing_update_modes_fields")) + } + + // Built-in skills cannot be modified + if (source === "built-in") { + throw new Error(t("skills:errors.cannot_modify_builtin")) + } + + const skillsManager = provider.getSkillsManager() + if (!skillsManager) { + throw new Error(t("skills:errors.manager_unavailable")) + } + + await skillsManager.updateSkillModes(skillName, source, newModeSlugs) + + // Send updated skills list + const skills = skillsManager.getSkillsMetadata() + await provider.postMessageToWebview({ type: "skills", skills }) + return skills + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Error updating skill modes: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to update skill modes: ${errorMessage}`) + return undefined + } +} + /** * Handles the openSkillFile message - opens a skill file in the editor */ @@ -159,7 +201,6 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv try { const skillName = message.skillName const source = message.source - const skillMode = message.skillMode if (!skillName || !source) { throw new Error(t("skills:errors.missing_delete_fields")) @@ -175,7 +216,8 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv throw new Error(t("skills:errors.manager_unavailable")) } - const skill = skillsManager.getSkill(skillName, source, skillMode) + // Find skill by name and source (skills may have modeSlugs arrays now) + const skill = skillsManager.findSkillByNameAndSource(skillName, source) if (!skill) { throw new Error(t("skills:errors.skill_not_found", { name: skillName })) } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index dfb604eb865..65b5956c88f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -37,6 +37,7 @@ import { handleCreateSkill, handleDeleteSkill, handleMoveSkill, + handleUpdateSkillModes, handleOpenSkillFile, } from "./skillsMessageHandler" import { changeLanguage, t } from "../../i18n" @@ -3007,6 +3008,10 @@ export const webviewMessageHandler = async ( await handleMoveSkill(provider, message) break } + case "updateSkillModes": { + await handleUpdateSkillModes(provider, message) + break + } case "openSkillFile": { await handleOpenSkillFile(provider, message) break diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json index 74d8cba0393..47b59938896 100644 --- a/src/i18n/locales/ca/skills.json +++ b/src/i18n/locales/ca/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 5aad37950f7..fe051288952 100644 --- a/src/i18n/locales/de/skills.json +++ b/src/i18n/locales/de/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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/en/skills.json b/src/i18n/locales/en/skills.json index ef4d7e68e3d..5b6dde45b98 100644 --- a/src/i18n/locales/en/skills.json +++ b/src/i18n/locales/en/skills.json @@ -8,6 +8,7 @@ "not_found": "Skill \"{{name}}\" not found in {{source}}{{modeInfo}}", "missing_create_fields": "Missing required fields: skillName, source, or skillDescription", "missing_move_fields": "Missing required fields: skillName or source", + "missing_update_modes_fields": "Missing required fields: skillName or source", "manager_unavailable": "Skills manager not available", "missing_delete_fields": "Missing required fields: skillName or source", "skill_not_found": "Skill \"{{name}}\" not found", diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json index 65345815182..84ab35b6d12 100644 --- a/src/i18n/locales/es/skills.json +++ b/src/i18n/locales/es/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 5c4cb1f5ae0..6320a4f55db 100644 --- a/src/i18n/locales/fr/skills.json +++ b/src/i18n/locales/fr/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 50929b48459..9b79cdb30f7 100644 --- a/src/i18n/locales/hi/skills.json +++ b/src/i18n/locales/hi/skills.json @@ -8,6 +8,7 @@ "not_found": "स्किल \"{{name}}\" {{source}}{{modeInfo}} में नहीं मिला", "missing_create_fields": "आवश्यक फ़ील्ड गायब हैं: skillName, source, या skillDescription", "missing_move_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", + "missing_update_modes_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 cfa01b33234..6559a9d6b1d 100644 --- a/src/i18n/locales/id/skills.json +++ b/src/i18n/locales/id/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 0ddcf0f70c0..fdfd82e261c 100644 --- a/src/i18n/locales/it/skills.json +++ b/src/i18n/locales/it/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 16576b2be35..074baacdfdf 100644 --- a/src/i18n/locales/ja/skills.json +++ b/src/i18n/locales/ja/skills.json @@ -8,6 +8,7 @@ "not_found": "スキル「{{name}}」が{{source}}{{modeInfo}}に見つかりません", "missing_create_fields": "必須フィールドが不足しています:skillName、source、またはskillDescription", "missing_move_fields": "必須フィールドが不足しています:skillNameまたはsource", + "missing_update_modes_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 c5808f3630b..5386675ea6e 100644 --- a/src/i18n/locales/ko/skills.json +++ b/src/i18n/locales/ko/skills.json @@ -8,6 +8,7 @@ "not_found": "{{source}}{{modeInfo}}에서 스킬 \"{{name}}\"을(를) 찾을 수 없습니다", "missing_create_fields": "필수 필드 누락: skillName, source 또는 skillDescription", "missing_move_fields": "필수 필드 누락: skillName 또는 source", + "missing_update_modes_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 6c6e7e0e832..ed9caab43b4 100644 --- a/src/i18n/locales/nl/skills.json +++ b/src/i18n/locales/nl/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 f9363e42d03..7a5e5f0ac18 100644 --- a/src/i18n/locales/pl/skills.json +++ b/src/i18n/locales/pl/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 8058e9f6a38..eac683e7fe7 100644 --- a/src/i18n/locales/pt-BR/skills.json +++ b/src/i18n/locales/pt-BR/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 627a8fd4d65..740d813873d 100644 --- a/src/i18n/locales/ru/skills.json +++ b/src/i18n/locales/ru/skills.json @@ -8,6 +8,7 @@ "not_found": "Навык \"{{name}}\" не найден в {{source}}{{modeInfo}}", "missing_create_fields": "Отсутствуют обязательные поля: skillName, source или skillDescription", "missing_move_fields": "Отсутствуют обязательные поля: skillName или source", + "missing_update_modes_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 e7781aa696b..235b9d55fc5 100644 --- a/src/i18n/locales/tr/skills.json +++ b/src/i18n/locales/tr/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 f97b7ed2b07..47e433ee579 100644 --- a/src/i18n/locales/vi/skills.json +++ b/src/i18n/locales/vi/skills.json @@ -8,6 +8,7 @@ "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", + "missing_update_modes_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 566f583feee..719bc722a5d 100644 --- a/src/i18n/locales/zh-CN/skills.json +++ b/src/i18n/locales/zh-CN/skills.json @@ -8,6 +8,7 @@ "not_found": "在 {{source}}{{modeInfo}} 中未找到技能 \"{{name}}\"", "missing_create_fields": "缺少必填字段:skillName、source 或 skillDescription", "missing_move_fields": "缺少必填字段:skillName 或 source", + "missing_update_modes_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 633bb1a6b2b..2d9a52be1e7 100644 --- a/src/i18n/locales/zh-TW/skills.json +++ b/src/i18n/locales/zh-TW/skills.json @@ -8,6 +8,7 @@ "not_found": "在 {{source}}{{modeInfo}} 中找不到技能「{{name}}」", "missing_create_fields": "缺少必填欄位:skillName、source 或 skillDescription", "missing_move_fields": "缺少必填欄位:skillName 或 source", + "missing_update_modes_fields": "缺少必填欄位:skillName 或 source", "manager_unavailable": "技能管理器無法使用", "missing_delete_fields": "缺少必填欄位:skillName 或 source", "skill_not_found": "找不到技能「{{name}}」", diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts index e23e2e1a390..66bf69b838e 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -148,15 +148,34 @@ export class SkillsManager { return } - // Create unique key combining name, source, and mode for override resolution - const skillKey = this.getSkillKey(effectiveSkillName, source, mode) + // Parse modeSlugs from frontmatter (new format) or fall back to directory-based mode + // Priority: frontmatter.modeSlugs > frontmatter.mode > directory mode + let modeSlugs: string[] | undefined + if (Array.isArray(frontmatter.modeSlugs)) { + modeSlugs = frontmatter.modeSlugs.filter((s: unknown) => typeof s === "string" && s.length > 0) + if (modeSlugs.length === 0) { + modeSlugs = undefined // Empty array means "any mode" + } + } else if (typeof frontmatter.mode === "string" && frontmatter.mode.length > 0) { + // Legacy single mode in frontmatter + modeSlugs = [frontmatter.mode] + } else if (mode) { + // Fall back to directory-based mode (skills-{mode}/) + modeSlugs = [mode] + } + + // Create unique key combining name, source, and modeSlugs for override resolution + // For backward compatibility, use first mode slug or undefined for the key + const primaryMode = modeSlugs?.[0] + const skillKey = this.getSkillKey(effectiveSkillName, source, primaryMode) this.skills.set(skillKey, { name: effectiveSkillName, description, path: skillMdPath, source, - mode, // undefined for generic skills, string for mode-specific + mode: primaryMode, // Deprecated: kept for backward compatibility + modeSlugs, // New: array of mode slugs, undefined = any mode }) } catch (error) { console.error(`Failed to load skill at ${skillDir}:`, error) @@ -179,8 +198,11 @@ export class SkillsManager { // Then, add discovered skills (will override built-in skills with same name) for (const skill of this.skills.values()) { - // Skip mode-specific skills that don't match current mode - if (skill.mode && skill.mode !== currentMode) continue + // Check if skill is available in current mode: + // - modeSlugs undefined or empty = available in all modes ("Any mode") + // - modeSlugs array with values = available only if currentMode is in the array + const isAvailableInMode = this.isSkillAvailableInMode(skill, currentMode) + if (!isAvailableInMode) continue const existingSkill = resolvedSkills.get(skill.name) @@ -199,6 +221,20 @@ export class SkillsManager { return Array.from(resolvedSkills.values()) } + /** + * Check if a skill is available in the given mode. + * - modeSlugs undefined or empty = available in all modes ("Any mode") + * - modeSlugs with values = available only if mode is in the array + */ + private isSkillAvailableInMode(skill: SkillMetadata, currentMode: string): boolean { + // No mode restrictions = available in all modes + if (!skill.modeSlugs || skill.modeSlugs.length === 0) { + return true + } + // Check if current mode is in the allowed modes + return skill.modeSlugs.includes(currentMode) + } + /** * Determine if newSkill should override existingSkill based on priority rules. * Priority: project > global > built-in, mode-specific > generic @@ -219,8 +255,11 @@ export class SkillsManager { if (newPriority < existingPriority) return false // Same source: mode-specific overrides generic - if (newSkill.mode && !existing.mode) return true - if (!newSkill.mode && existing.mode) return false + // A skill with modeSlugs (restricted) is more specific than one without (any mode) + const existingHasModes = existing.modeSlugs && existing.modeSlugs.length > 0 + const newHasModes = newSkill.modeSlugs && newSkill.modeSlugs.length > 0 + if (newHasModes && !existingHasModes) return true + if (!newHasModes && existingHasModes) return false // Same source and same mode-specificity: keep existing (first wins) return false @@ -281,6 +320,19 @@ export class SkillsManager { return this.skills.get(skillKey) } + /** + * Find a skill by name and source (regardless of mode). + * Useful for opening/editing skills where the exact mode key may vary. + */ + findSkillByNameAndSource(name: string, source: "global" | "project"): SkillMetadata | undefined { + for (const skill of this.skills.values()) { + if (skill.name === name && skill.source === source) { + return skill + } + } + return undefined + } + /** * Validate skill name per agentskills.io spec using shared validation. * Converts error codes to user-friendly error messages. @@ -312,10 +364,15 @@ export class SkillsManager { * @param name - Skill name (must be valid per agentskills.io spec) * @param source - "global" or "project" * @param description - Skill description - * @param mode - Optional mode restriction (creates in skills-{mode}/ directory) + * @param modeSlugs - Optional mode restrictions (undefined/empty = any mode) * @returns Path to created SKILL.md file */ - async createSkill(name: string, source: "global" | "project", description: string, mode?: string): Promise { + async createSkill( + name: string, + source: "global" | "project", + description: string, + modeSlugs?: string[], + ): Promise { // Validate skill name const validation = this.validateSkillName(name) if (!validation.valid) { @@ -340,9 +397,8 @@ export class SkillsManager { baseDir = path.join(provider.cwd, ".roo") } - // Determine skills directory (with optional mode suffix) - const skillsDirName = mode ? `skills-${mode}` : "skills" - const skillsDir = path.join(baseDir, skillsDirName) + // Always use the generic skills directory (mode info stored in frontmatter now) + const skillsDir = path.join(baseDir, "skills") const skillDir = path.join(skillsDir, name) const skillMdPath = path.join(skillDir, "SKILL.md") @@ -360,9 +416,17 @@ export class SkillsManager { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" ") + // Build frontmatter with optional modeSlugs + const frontmatterLines = [`name: ${name}`, `description: ${trimmedDescription}`] + if (modeSlugs && modeSlugs.length > 0) { + frontmatterLines.push(`modeSlugs:`) + for (const slug of modeSlugs) { + frontmatterLines.push(` - ${slug}`) + } + } + const skillContent = `--- -name: ${name} -description: ${trimmedDescription} +${frontmatterLines.join("\n")} --- # ${titleName} @@ -476,6 +540,49 @@ Add your skill instructions here. await this.discoverSkills() } + /** + * Update the mode associations for a skill by modifying its SKILL.md frontmatter. + * @param name - Skill name + * @param source - Where the skill is located ("global" or "project") + * @param newModeSlugs - New mode slugs (undefined/empty = any mode) + */ + async updateSkillModes(name: string, source: "global" | "project", newModeSlugs?: string[]): Promise { + // Find any skill with this name and source (regardless of current mode) + let skill: SkillMetadata | undefined + for (const s of this.skills.values()) { + if (s.name === name && s.source === source) { + skill = s + break + } + } + + if (!skill) { + throw new Error(t("skills:errors.not_found", { name, source, modeInfo: "" })) + } + + // Read the current SKILL.md file + const fileContent = await fs.readFile(skill.path, "utf-8") + const { data: frontmatter, content: body } = matter(fileContent) + + // Update the frontmatter with new modeSlugs + if (newModeSlugs && newModeSlugs.length > 0) { + frontmatter.modeSlugs = newModeSlugs + // Remove legacy mode field if present + delete frontmatter.mode + } else { + // Empty/undefined = any mode, remove mode restrictions + delete frontmatter.modeSlugs + delete frontmatter.mode + } + + // Serialize back to SKILL.md format + const newContent = matter.stringify(body, frontmatter) + await fs.writeFile(skill.path, newContent, "utf-8") + + // 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 189ca41b51a..4abf4717be8 100644 --- a/src/services/skills/__tests__/SkillsManager.spec.ts +++ b/src/services/skills/__tests__/SkillsManager.spec.ts @@ -1206,7 +1206,7 @@ Instructions`) expect(writeCall[1]).toContain("description: A new skill description") }) - it("should create a mode-specific skill", async () => { + it("should create a mode-specific skill with modeSlugs array", async () => { mockDirectoryExists.mockResolvedValue(false) mockRealpath.mockImplementation(async (p: string) => p) mockReaddir.mockResolvedValue([]) @@ -1214,9 +1214,15 @@ Instructions`) mockMkdir.mockResolvedValue(undefined) mockWriteFile.mockResolvedValue(undefined) - const createdPath = await skillsManager.createSkill("code-skill", "global", "A code skill", "code") + const createdPath = await skillsManager.createSkill("code-skill", "global", "A code skill", ["code"]) - expect(createdPath).toBe(p(GLOBAL_ROO_DIR, "skills-code", "code-skill", "SKILL.md")) + // Skills are always created in the generic skills directory now; mode info is in frontmatter + expect(createdPath).toBe(p(GLOBAL_ROO_DIR, "skills", "code-skill", "SKILL.md")) + + // Verify frontmatter contains modeSlugs + const writeCall = mockWriteFile.mock.calls[0] + expect(writeCall[1]).toContain("modeSlugs:") + expect(writeCall[1]).toContain("- code") }) it("should create a project skill", async () => { diff --git a/src/shared/skills.ts b/src/shared/skills.ts index ae35b8c3878..cbcc71d7b78 100644 --- a/src/shared/skills.ts +++ b/src/shared/skills.ts @@ -7,7 +7,17 @@ export interface SkillMetadata { description: string // Required: when to use this skill path: string // Absolute path to SKILL.md (or "" for built-in skills) source: "global" | "project" | "built-in" // Where the skill was discovered - mode?: string // If set, skill is only available in this mode + /** + * @deprecated Use modeSlugs instead. Kept for backward compatibility. + * If set, skill is only available in this mode. + */ + mode?: string + /** + * Mode slugs where this skill is available. + * - undefined or empty array means the skill is available in all modes ("Any mode"). + * - An array with one or more mode slugs restricts the skill to those modes. + */ + modeSlugs?: string[] } /** diff --git a/webview-ui/src/components/chat/SlashCommandItem.tsx b/webview-ui/src/components/chat/SlashCommandItem.tsx deleted file mode 100644 index 04ade08bbd1..00000000000 --- a/webview-ui/src/components/chat/SlashCommandItem.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react" -import { Edit, Trash2 } from "lucide-react" - -import type { Command } from "@roo-code/types" - -import { useAppTranslation } from "@/i18n/TranslationContext" -import { Button, StandardTooltip } from "@/components/ui" -import { vscode } from "@/utils/vscode" - -interface SlashCommandItemProps { - command: Command - onDelete: (command: Command) => void - onClick?: (command: Command) => void -} - -export const SlashCommandItem: React.FC = ({ command, onDelete, onClick }) => { - const { t } = useAppTranslation() - - // Built-in commands cannot be edited or deleted - const isBuiltIn = command.source === "built-in" - - const handleEdit = () => { - if (command.filePath) { - vscode.postMessage({ - type: "openFile", - text: command.filePath, - }) - } else { - // Fallback: request to open command file by name and source - vscode.postMessage({ - type: "openCommandFile", - text: command.name, - values: { source: command.source }, - }) - } - } - - const handleDelete = () => { - onDelete(command) - } - - return ( -
- {/* Command name - clickable */} -
onClick?.(command)}> -
- {command.name} - {command.description && ( -
- {command.description} -
- )} -
-
- - {/* Action buttons - only show for non-built-in commands */} - {!isBuiltIn && ( -
- - - - - - - -
- )} -
- ) -} diff --git a/webview-ui/src/components/settings/CreateSkillDialog.tsx b/webview-ui/src/components/settings/CreateSkillDialog.tsx new file mode 100644 index 00000000000..3a8def14ee0 --- /dev/null +++ b/webview-ui/src/components/settings/CreateSkillDialog.tsx @@ -0,0 +1,289 @@ +import React, { useState, useCallback, useMemo } from "react" +import { validateSkillName as validateSkillNameShared, SkillNameValidationError } from "@roo-code/types" + +import { getAllModes } from "@roo/modes" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + Button, + Checkbox, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Textarea, +} from "@/components/ui" +import { vscode } from "@/utils/vscode" + +interface CreateSkillDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSkillCreated: () => void + hasWorkspace: boolean +} + +/** + * Map skill name validation error codes to translation keys. + */ +const getSkillNameErrorTranslationKey = (error: SkillNameValidationError): string => { + switch (error) { + case SkillNameValidationError.Empty: + return "settings:skills.validation.nameRequired" + case SkillNameValidationError.TooLong: + return "settings:skills.validation.nameTooLong" + case SkillNameValidationError.InvalidFormat: + return "settings:skills.validation.nameInvalid" + } +} + +/** + * Validate skill name using shared validation from @roo-code/types. + * Returns a translation key for the error, or null if valid. + */ +const validateSkillName = (name: string): string | null => { + const result = validateSkillNameShared(name) + if (!result.valid) { + return getSkillNameErrorTranslationKey(result.error!) + } + return null +} + +/** + * Validate description according to agentskills.io spec: + * - Required field + * - 1-1024 characters + */ +const validateDescription = (description: string): string | null => { + if (!description) return "settings:skills.validation.descriptionRequired" + if (description.length > 1024) return "settings:skills.validation.descriptionTooLong" + return null +} + +export const CreateSkillDialog: React.FC = ({ + open, + onOpenChange, + onSkillCreated, + hasWorkspace, +}) => { + const { t } = useAppTranslation() + const { customModes } = useExtensionState() + + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [source, setSource] = useState<"global" | "project">(hasWorkspace ? "project" : "global") + const [nameError, setNameError] = useState(null) + const [descriptionError, setDescriptionError] = useState(null) + + // Multi-mode selection state (same pattern as SkillsSettings mode dialog) + const [selectedModes, setSelectedModes] = useState([]) + const [isAnyMode, setIsAnyMode] = useState(true) + + // Get available modes for the checkboxes (built-in + custom modes) + const availableModes = useMemo(() => { + return getAllModes(customModes).map((m) => ({ slug: m.slug, name: m.name })) + }, [customModes]) + + const resetForm = useCallback(() => { + setName("") + setDescription("") + setSource(hasWorkspace ? "project" : "global") + setSelectedModes([]) + setIsAnyMode(true) + setNameError(null) + setDescriptionError(null) + }, [hasWorkspace]) + + const handleClose = useCallback(() => { + resetForm() + onOpenChange(false) + }, [resetForm, onOpenChange]) + + const handleNameChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "") + setName(value) + setNameError(null) + }, []) + + const handleDescriptionChange = useCallback((e: React.ChangeEvent) => { + setDescription(e.target.value) + setDescriptionError(null) + }, []) + + // Handle "Any mode" toggle - mutually exclusive with specific modes + const handleAnyModeToggle = useCallback((checked: boolean) => { + if (checked) { + setIsAnyMode(true) + setSelectedModes([]) // Clear specific modes when "Any mode" is selected + } else { + setIsAnyMode(false) + } + }, []) + + // Handle specific mode toggle - unchecks "Any mode" when a specific mode is selected + const handleModeToggle = useCallback((modeSlug: string, checked: boolean) => { + if (checked) { + setIsAnyMode(false) // Uncheck "Any mode" when selecting a specific mode + setSelectedModes((prev) => [...prev, modeSlug]) + } else { + setSelectedModes((prev) => { + const newModes = prev.filter((m) => m !== modeSlug) + // If no modes selected, default back to "Any mode" + if (newModes.length === 0) { + setIsAnyMode(true) + } + return newModes + }) + } + }, []) + + const handleCreate = useCallback(() => { + // Validate fields + const nameValidationError = validateSkillName(name) + const descValidationError = validateDescription(description) + + if (nameValidationError) { + setNameError(nameValidationError) + return + } + + if (descValidationError) { + setDescriptionError(descValidationError) + return + } + + // Send message to create skill + // Convert to modeSlugs: undefined for "Any mode", or array of selected modes + const modeSlugs = isAnyMode ? undefined : selectedModes.length > 0 ? selectedModes : undefined + vscode.postMessage({ + type: "createSkill", + skillName: name, + source, + skillDescription: description, + skillModeSlugs: modeSlugs, + }) + + // Close dialog and notify parent + handleClose() + onSkillCreated() + }, [name, description, source, isAnyMode, selectedModes, handleClose, onSkillCreated]) + + return ( + + + + {t("settings:skills.createDialog.title")} + + + +
+ {/* Name Input */} +
+ + + {nameError && {t(nameError)}} +
+ + {/* Description Input */} +
+