mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
c983e26280
commit
010aba24b7
56 changed files with 3879 additions and 44 deletions
144
packages/types/src/__tests__/skills.test.ts
Normal file
144
packages/types/src/__tests__/skills.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import {
|
||||
validateSkillName,
|
||||
SkillNameValidationError,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
SKILL_NAME_REGEX,
|
||||
} from "../skills.js"
|
||||
|
||||
describe("validateSkillName", () => {
|
||||
describe("valid names", () => {
|
||||
it("accepts single lowercase word", () => {
|
||||
expect(validateSkillName("myskill")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts lowercase letters and numbers", () => {
|
||||
expect(validateSkillName("skill123")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts hyphenated words", () => {
|
||||
expect(validateSkillName("my-skill")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts multiple hyphenated words", () => {
|
||||
expect(validateSkillName("my-awesome-skill")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts single character", () => {
|
||||
expect(validateSkillName("a")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts single digit", () => {
|
||||
expect(validateSkillName("1")).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
it("accepts maximum length name (64 characters)", () => {
|
||||
const maxLengthName = "a".repeat(SKILL_NAME_MAX_LENGTH)
|
||||
expect(validateSkillName(maxLengthName)).toEqual({ valid: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty or missing names", () => {
|
||||
it("rejects empty string", () => {
|
||||
expect(validateSkillName("")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.Empty,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("names that are too long", () => {
|
||||
it("rejects names longer than 64 characters", () => {
|
||||
const tooLongName = "a".repeat(SKILL_NAME_MAX_LENGTH + 1)
|
||||
expect(validateSkillName(tooLongName)).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.TooLong,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid format", () => {
|
||||
it("rejects uppercase letters", () => {
|
||||
expect(validateSkillName("MySkill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects leading hyphen", () => {
|
||||
expect(validateSkillName("-myskill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects trailing hyphen", () => {
|
||||
expect(validateSkillName("myskill-")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects consecutive hyphens", () => {
|
||||
expect(validateSkillName("my--skill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects spaces", () => {
|
||||
expect(validateSkillName("my skill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects underscores", () => {
|
||||
expect(validateSkillName("my_skill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects special characters", () => {
|
||||
expect(validateSkillName("my@skill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects dots", () => {
|
||||
expect(validateSkillName("my.skill")).toEqual({
|
||||
valid: false,
|
||||
error: SkillNameValidationError.InvalidFormat,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("SKILL_NAME_REGEX", () => {
|
||||
it("matches valid names", () => {
|
||||
expect(SKILL_NAME_REGEX.test("myskill")).toBe(true)
|
||||
expect(SKILL_NAME_REGEX.test("my-skill")).toBe(true)
|
||||
expect(SKILL_NAME_REGEX.test("skill123")).toBe(true)
|
||||
expect(SKILL_NAME_REGEX.test("a1-b2-c3")).toBe(true)
|
||||
})
|
||||
|
||||
it("does not match invalid names", () => {
|
||||
expect(SKILL_NAME_REGEX.test("-start")).toBe(false)
|
||||
expect(SKILL_NAME_REGEX.test("end-")).toBe(false)
|
||||
expect(SKILL_NAME_REGEX.test("double--hyphen")).toBe(false)
|
||||
expect(SKILL_NAME_REGEX.test("UPPER")).toBe(false)
|
||||
expect(SKILL_NAME_REGEX.test("")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("constants", () => {
|
||||
it("has correct min length", () => {
|
||||
expect(SKILL_NAME_MIN_LENGTH).toBe(1)
|
||||
})
|
||||
|
||||
it("has correct max length", () => {
|
||||
expect(SKILL_NAME_MAX_LENGTH).toBe(64)
|
||||
})
|
||||
})
|
||||
|
|
@ -19,6 +19,7 @@ export * from "./message.js"
|
|||
export * from "./mode.js"
|
||||
export * from "./model.js"
|
||||
export * from "./provider-settings.js"
|
||||
export * from "./skills.js"
|
||||
export * from "./task.js"
|
||||
export * from "./todo.js"
|
||||
export * from "./telemetry.js"
|
||||
|
|
|
|||
71
packages/types/src/skills.ts
Normal file
71
packages/types/src/skills.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* 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
|
||||
source: "global" | "project" // Where the skill was discovered
|
||||
mode?: string // If set, skill is only available in this mode
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList,
|
|||
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
|
||||
import type { GitCommit } from "./git.js"
|
||||
import type { McpServer } from "./mcp.js"
|
||||
import type { SkillMetadata } from "./skills.js"
|
||||
import type { ModelRecord, RouterModels } from "./model.js"
|
||||
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
|
||||
import type { WorktreeIncludeStatus } from "./worktree.js"
|
||||
|
|
@ -108,6 +109,7 @@ export interface ExtensionMessage {
|
|||
| "worktreeIncludeStatus"
|
||||
| "branchWorktreeIncludeResult"
|
||||
| "folderSelected"
|
||||
| "skills"
|
||||
text?: string
|
||||
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
checkpointWarning?: {
|
||||
|
|
@ -202,6 +204,7 @@ export interface ExtensionMessage {
|
|||
stepIndex?: number // For browserSessionNavigate: the target step index to display
|
||||
tools?: SerializedCustomToolDefinition[] // For customToolsResult
|
||||
modes?: { slug: string; name: string }[] // For modes response
|
||||
skills?: SkillMetadata[] // For skills response
|
||||
aggregatedCosts?: {
|
||||
// For taskWithAggregatedCosts response
|
||||
totalCost: number
|
||||
|
|
@ -602,6 +605,11 @@ export interface WebviewMessage {
|
|||
| "createWorktreeInclude"
|
||||
| "checkoutBranch"
|
||||
| "browseForWorktreePath"
|
||||
// Skills messages
|
||||
| "requestSkills"
|
||||
| "createSkill"
|
||||
| "deleteSkill"
|
||||
| "openSkillFile"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
@ -636,6 +644,9 @@ export interface WebviewMessage {
|
|||
timeout?: number
|
||||
payload?: WebViewMessagePayload
|
||||
source?: "global" | "project"
|
||||
skillName?: string // For skill operations (createSkill, deleteSkill, openSkillFile)
|
||||
skillMode?: string // For skill operations (mode restriction)
|
||||
skillDescription?: string // For createSkill (skill description)
|
||||
requestId?: string
|
||||
ids?: string[]
|
||||
hasSystemPromptOverride?: boolean
|
||||
|
|
|
|||
334
src/core/webview/__tests__/skillsMessageHandler.spec.ts
Normal file
334
src/core/webview/__tests__/skillsMessageHandler.spec.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// 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<string, any>) => {
|
||||
const translations: Record<string, string> = {
|
||||
"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.skill_not_found": `Skill "${params?.name}" not found`,
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}))
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { openFile } from "../../../integrations/misc/open-file"
|
||||
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, 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 mockGetSkill = vi.fn()
|
||||
|
||||
const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
|
||||
const skillsManager = hasSkillsManager
|
||||
? {
|
||||
getSkillsMetadata: mockGetSkillsMetadata,
|
||||
createSkill: mockCreateSkill,
|
||||
deleteSkill: mockDeleteSkill,
|
||||
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("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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
133
src/core/webview/skillsMessageHandler.ts
Normal file
133
src/core/webview/skillsMessageHandler.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
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<SkillMetadata[]> {
|
||||
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<SkillMetadata[] | undefined> {
|
||||
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"))
|
||||
}
|
||||
|
||||
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<SkillMetadata[] | undefined> {
|
||||
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"))
|
||||
}
|
||||
|
||||
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 openSkillFile message - opens a skill file in the editor
|
||||
*/
|
||||
export async function handleOpenSkillFile(provider: ClineProvider, message: WebviewMessage): Promise<void> {
|
||||
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"))
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import { ClineProvider } from "./ClineProvider"
|
|||
import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
|
||||
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
|
||||
import { generateErrorDiagnostics } from "./diagnosticsHandler"
|
||||
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "./skillsMessageHandler"
|
||||
import { changeLanguage, t } from "../../i18n"
|
||||
import { Package } from "../../shared/package"
|
||||
import { type RouterName, toRouterName } from "../../shared/api"
|
||||
|
|
@ -2974,6 +2975,22 @@ 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 "openSkillFile": {
|
||||
await handleOpenSkillFile(provider, message)
|
||||
break
|
||||
}
|
||||
case "openCommandFile": {
|
||||
try {
|
||||
if (message.text) {
|
||||
|
|
|
|||
14
src/i18n/locales/ca/skills.json
generated
Normal file
14
src/i18n/locales/ca/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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}}\""
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/de/skills.json
generated
Normal file
14
src/i18n/locales/de/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Skill-Manager nicht verfügbar",
|
||||
"missing_delete_fields": "Erforderliche Felder fehlen: skillName oder source",
|
||||
"skill_not_found": "Skill \"{{name}}\" nicht gefunden"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/en/skills.json
Normal file
14
src/i18n/locales/en/skills.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Skills manager not available",
|
||||
"missing_delete_fields": "Missing required fields: skillName or source",
|
||||
"skill_not_found": "Skill \"{{name}}\" not found"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/es/skills.json
generated
Normal file
14
src/i18n/locales/es/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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}}\""
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/fr/skills.json
generated
Normal file
14
src/i18n/locales/fr/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/hi/skills.json
generated
Normal file
14
src/i18n/locales/hi/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "स्किल मैनेजर उपलब्ध नहीं है",
|
||||
"missing_delete_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source",
|
||||
"skill_not_found": "स्किल \"{{name}}\" नहीं मिला"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/id/skills.json
generated
Normal file
14
src/i18n/locales/id/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Manajer skill tidak tersedia",
|
||||
"missing_delete_fields": "Bidang wajib tidak ada: skillName atau source",
|
||||
"skill_not_found": "Skill \"{{name}}\" tidak ditemukan"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/it/skills.json
generated
Normal file
14
src/i18n/locales/it/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Il gestore delle skill non è disponibile",
|
||||
"missing_delete_fields": "Campi obbligatori mancanti: skillName o source",
|
||||
"skill_not_found": "Skill \"{{name}}\" non trovata"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/ja/skills.json
generated
Normal file
14
src/i18n/locales/ja/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "スキルマネージャーが利用できません",
|
||||
"missing_delete_fields": "必須フィールドが不足しています:skillNameまたはsource",
|
||||
"skill_not_found": "スキル「{{name}}」が見つかりません"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/ko/skills.json
generated
Normal file
14
src/i18n/locales/ko/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "스킬 관리자를 사용할 수 없습니다",
|
||||
"missing_delete_fields": "필수 필드 누락: skillName 또는 source",
|
||||
"skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/nl/skills.json
generated
Normal file
14
src/i18n/locales/nl/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Vaardigheidenbeheerder niet beschikbaar",
|
||||
"missing_delete_fields": "Vereiste velden ontbreken: skillName of source",
|
||||
"skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/pl/skills.json
generated
Normal file
14
src/i18n/locales/pl/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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}}\""
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/pt-BR/skills.json
generated
Normal file
14
src/i18n/locales/pt-BR/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/ru/skills.json
generated
Normal file
14
src/i18n/locales/ru/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Менеджер навыков недоступен",
|
||||
"missing_delete_fields": "Отсутствуют обязательные поля: skillName или source",
|
||||
"skill_not_found": "Навык \"{{name}}\" не найден"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/tr/skills.json
generated
Normal file
14
src/i18n/locales/tr/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "Beceri yöneticisi kullanılamıyor",
|
||||
"missing_delete_fields": "Gerekli alanlar eksik: skillName veya source",
|
||||
"skill_not_found": "\"{{name}}\" becerisi bulunamadı"
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/vi/skills.json
generated
Normal file
14
src/i18n/locales/vi/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"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}}\""
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/zh-CN/skills.json
generated
Normal file
14
src/i18n/locales/zh-CN/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "技能管理器不可用",
|
||||
"missing_delete_fields": "缺少必填字段:skillName 或 source",
|
||||
"skill_not_found": "未找到技能 \"{{name}}\""
|
||||
}
|
||||
}
|
||||
14
src/i18n/locales/zh-TW/skills.json
generated
Normal file
14
src/i18n/locales/zh-TW/skills.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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",
|
||||
"manager_unavailable": "技能管理器無法使用",
|
||||
"missing_delete_fields": "缺少必填欄位:skillName 或 source",
|
||||
"skill_not_found": "找不到技能「{{name}}」"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import matter from "gray-matter"
|
||||
|
||||
|
|
@ -8,6 +9,12 @@ import { getGlobalRooDirectory } from "../roo-config"
|
|||
import { directoryExists, fileExists } from "../roo-config"
|
||||
import { SkillMetadata, SkillContent } from "../../shared/skills"
|
||||
import { modes, getAllModes } from "../../shared/modes"
|
||||
import {
|
||||
validateSkillName as validateSkillNameShared,
|
||||
SkillNameValidationError,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
} from "@roo-code/types"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
// Re-export for convenience
|
||||
export type { SkillMetadata, SkillContent }
|
||||
|
|
@ -116,23 +123,11 @@ export class SkillsManager {
|
|||
return
|
||||
}
|
||||
|
||||
// Strict spec validation (https://agentskills.io/specification)
|
||||
// Name constraints:
|
||||
// - 1-64 chars
|
||||
// - lowercase letters/numbers/hyphens only
|
||||
// - must not start/end with hyphen
|
||||
// - must not contain consecutive hyphens
|
||||
if (effectiveSkillName.length < 1 || effectiveSkillName.length > 64) {
|
||||
console.error(
|
||||
`Skill name "${effectiveSkillName}" is invalid: name must be 1-64 characters (got ${effectiveSkillName.length})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
const nameFormat = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
||||
if (!nameFormat.test(effectiveSkillName)) {
|
||||
console.error(
|
||||
`Skill name "${effectiveSkillName}" is invalid: must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)`,
|
||||
)
|
||||
// Validate skill name per agentskills.io spec using shared validation
|
||||
const nameValidation = validateSkillNameShared(effectiveSkillName)
|
||||
if (!nameValidation.valid) {
|
||||
const errorMessage = this.getSkillNameErrorMessage(effectiveSkillName, nameValidation.error!)
|
||||
console.error(`Skill name "${effectiveSkillName}" is invalid: ${errorMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +234,146 @@ 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<string> {
|
||||
// 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<void> {
|
||||
// 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all skills directories to scan, including mode-specific directories.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
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,
|
||||
} = 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(),
|
||||
}))
|
||||
|
||||
// Platform-agnostic test paths
|
||||
// Use forward slashes for consistency, then normalize with path.normalize
|
||||
|
|
@ -28,11 +41,17 @@ vi.mock("fs/promises", () => ({
|
|||
readFile: mockReadFile,
|
||||
readdir: mockReaddir,
|
||||
realpath: mockRealpath,
|
||||
mkdir: mockMkdir,
|
||||
writeFile: mockWriteFile,
|
||||
rm: mockRm,
|
||||
},
|
||||
stat: mockStat,
|
||||
readFile: mockReadFile,
|
||||
readdir: mockReaddir,
|
||||
realpath: mockRealpath,
|
||||
mkdir: mockMkdir,
|
||||
writeFile: mockWriteFile,
|
||||
rm: mockRm,
|
||||
}))
|
||||
|
||||
// Mock os module
|
||||
|
|
@ -63,6 +82,22 @@ vi.mock("../../roo-config", () => ({
|
|||
fileExists: mockFileExists,
|
||||
}))
|
||||
|
||||
// Mock i18n
|
||||
vi.mock("../../../i18n", () => ({
|
||||
t: (key: string, params?: Record<string, any>) => {
|
||||
const translations: Record<string, string> = {
|
||||
"skills:errors.name_length": `Skill name must be 1-${params?.maxLength} characters (got ${params?.length})`,
|
||||
"skills:errors.name_format":
|
||||
"Skill name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)",
|
||||
"skills:errors.description_length": `Skill description must be 1-1024 characters (got ${params?.length})`,
|
||||
"skills:errors.no_workspace": "Cannot create project skill: no workspace folder is open",
|
||||
"skills:errors.already_exists": `Skill "${params?.name}" already exists at ${params?.path}`,
|
||||
"skills:errors.not_found": `Skill "${params?.name}" not found in ${params?.source}${params?.modeInfo}`,
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}))
|
||||
|
||||
import { SkillsManager } from "../SkillsManager"
|
||||
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||
|
||||
|
|
@ -827,4 +862,268 @@ 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
254
webview-ui/src/components/settings/CreateSkillDialog.tsx
Normal file
254
webview-ui/src/components/settings/CreateSkillDialog.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
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,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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
|
||||
}
|
||||
|
||||
// Sentinel value for "Any mode" since Radix Select doesn't allow empty string values
|
||||
const MODE_ANY = "__any__"
|
||||
|
||||
export const CreateSkillDialog: React.FC<CreateSkillDialogProps> = ({
|
||||
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 [mode, setMode] = useState<string>(MODE_ANY)
|
||||
const [nameError, setNameError] = useState<string | null>(null)
|
||||
const [descriptionError, setDescriptionError] = useState<string | null>(null)
|
||||
|
||||
// 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])
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setName("")
|
||||
setDescription("")
|
||||
setSource(hasWorkspace ? "project" : "global")
|
||||
setMode(MODE_ANY)
|
||||
setNameError(null)
|
||||
setDescriptionError(null)
|
||||
}, [hasWorkspace])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
resetForm()
|
||||
onOpenChange(false)
|
||||
}, [resetForm, onOpenChange])
|
||||
|
||||
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "")
|
||||
setName(value)
|
||||
setNameError(null)
|
||||
}, [])
|
||||
|
||||
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(e.target.value)
|
||||
setDescriptionError(null)
|
||||
}, [])
|
||||
|
||||
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 MODE_ANY sentinel value to undefined for the backend
|
||||
vscode.postMessage({
|
||||
type: "createSkill",
|
||||
skillName: name,
|
||||
source,
|
||||
skillDescription: description,
|
||||
skillMode: mode === MODE_ANY ? undefined : mode,
|
||||
})
|
||||
|
||||
// Close dialog and notify parent
|
||||
handleClose()
|
||||
onSkillCreated()
|
||||
}, [name, description, source, mode, handleClose, onSkillCreated])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("settings:skills.createDialog.title")}</DialogTitle>
|
||||
<DialogDescription>{t("settings:skills.createDialog.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
{/* Name Input */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="skill-name" className="text-sm font-medium text-vscode-foreground">
|
||||
{t("settings:skills.createDialog.nameLabel")} *
|
||||
</label>
|
||||
<input
|
||||
id="skill-name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t("settings:skills.createDialog.namePlaceholder")}
|
||||
maxLength={64}
|
||||
className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-3 py-2 text-sm focus:outline-none focus:border-vscode-focusBorder"
|
||||
/>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:skills.createDialog.nameHint")}
|
||||
</span>
|
||||
{nameError && <span className="text-xs text-vscode-errorForeground">{t(nameError)}</span>}
|
||||
</div>
|
||||
|
||||
{/* Description Input */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="skill-description" className="text-sm font-medium text-vscode-foreground">
|
||||
{t("settings:skills.createDialog.descriptionLabel")} *
|
||||
</label>
|
||||
<textarea
|
||||
id="skill-description"
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
placeholder={t("settings:skills.createDialog.descriptionPlaceholder")}
|
||||
maxLength={1024}
|
||||
rows={3}
|
||||
className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-3 py-2 text-sm focus:outline-none focus:border-vscode-focusBorder resize-none"
|
||||
/>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:skills.createDialog.descriptionHint")}
|
||||
</span>
|
||||
{descriptionError && (
|
||||
<span className="text-xs text-vscode-errorForeground">{t(descriptionError)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Source Selection */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-vscode-foreground">
|
||||
{t("settings:skills.createDialog.sourceLabel")}
|
||||
</label>
|
||||
<Select value={source} onValueChange={(value) => setSource(value as "global" | "project")}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">{t("settings:skills.source.global")}</SelectItem>
|
||||
{hasWorkspace && (
|
||||
<SelectItem value="project">{t("settings:skills.source.project")}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:skills.createDialog.sourceHint")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mode Selection (Optional) */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-vscode-foreground">
|
||||
{t("settings:skills.createDialog.modeLabel")}
|
||||
</label>
|
||||
<Select value={mode} onValueChange={setMode}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:skills.createDialog.modePlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={MODE_ANY}>{t("settings:skills.createDialog.modeAny")}</SelectItem>
|
||||
{availableModes.map((m) => (
|
||||
<SelectItem key={m.slug} value={m.slug}>
|
||||
{m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:skills.createDialog.modeHint")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
{t("settings:skills.createDialog.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleCreate} disabled={!name || !description}>
|
||||
{t("settings:skills.createDialog.create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import {
|
|||
Users2,
|
||||
ArrowLeft,
|
||||
GitCommitVertical,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
|
|
@ -77,6 +78,7 @@ import { About } from "./About"
|
|||
import { Section } from "./Section"
|
||||
import PromptsSettings from "./PromptsSettings"
|
||||
import { SlashCommandsSettings } from "./SlashCommandsSettings"
|
||||
import { SkillsSettings } from "./SkillsSettings"
|
||||
import { UISettings } from "./UISettings"
|
||||
import ModesView from "../modes/ModesView"
|
||||
import McpView from "../mcp/McpView"
|
||||
|
|
@ -99,6 +101,7 @@ export const sectionNames = [
|
|||
"providers",
|
||||
"autoApprove",
|
||||
"slashCommands",
|
||||
"skills",
|
||||
"browser",
|
||||
"checkpoints",
|
||||
"notifications",
|
||||
|
|
@ -516,6 +519,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
{ id: "mcp", icon: Server },
|
||||
{ id: "autoApprove", icon: CheckCheck },
|
||||
{ id: "slashCommands", icon: SquareSlash },
|
||||
{ id: "skills", icon: Zap },
|
||||
{ id: "browser", icon: SquareMousePointer },
|
||||
{ id: "checkpoints", icon: GitCommitVertical },
|
||||
{ id: "notifications", icon: Bell },
|
||||
|
|
@ -806,6 +810,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
{/* Slash Commands Section */}
|
||||
{renderTab === "slashCommands" && <SlashCommandsSettings />}
|
||||
|
||||
{/* Skills Section */}
|
||||
{renderTab === "skills" && <SkillsSettings />}
|
||||
|
||||
{/* Browser Section */}
|
||||
{renderTab === "browser" && (
|
||||
<BrowserSettings
|
||||
|
|
|
|||
61
webview-ui/src/components/settings/SkillItem.tsx
Normal file
61
webview-ui/src/components/settings/SkillItem.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from "react"
|
||||
import { Edit, Trash2 } from "lucide-react"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Button, StandardTooltip } from "@/components/ui"
|
||||
|
||||
interface SkillItemProps {
|
||||
skill: SkillMetadata
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
export const SkillItem: React.FC<SkillItemProps> = ({ skill, onEdit, onDelete }) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2 text-sm flex items-center group hover:bg-vscode-list-hoverBackground">
|
||||
{/* Skill name and description */}
|
||||
<div className="flex-1 min-w-0 cursor-pointer" onClick={onEdit}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-vscode-foreground">{skill.name}</span>
|
||||
{skill.mode && (
|
||||
<span className="px-1.5 py-0.5 text-xs rounded bg-vscode-badge-background text-vscode-badge-foreground shrink-0">
|
||||
{skill.mode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{skill.description && (
|
||||
<div className="text-xs text-vscode-descriptionForeground truncate mt-0.5">{skill.description}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<StandardTooltip content={t("settings:skills.editSkill")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
tabIndex={-1}
|
||||
onClick={onEdit}
|
||||
className="size-6 flex items-center justify-center opacity-60 hover:opacity-100">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
|
||||
<StandardTooltip content={t("settings:skills.deleteSkill")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
tabIndex={-1}
|
||||
onClick={onDelete}
|
||||
className="size-6 flex items-center justify-center opacity-60 hover:opacity-100 hover:text-red-400">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
228
webview-ui/src/components/settings/SkillsSettings.tsx
Normal file
228
webview-ui/src/components/settings/SkillsSettings.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { Plus, Globe, Folder } from "lucide-react"
|
||||
import { Trans } from "react-i18next"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
} from "@/components/ui"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { buildDocLink } from "@/utils/docLinks"
|
||||
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { Section } from "./Section"
|
||||
import { SearchableSetting } from "./SearchableSetting"
|
||||
import { SkillItem } from "./SkillItem"
|
||||
import { CreateSkillDialog } from "./CreateSkillDialog"
|
||||
import type { SectionName } from "./SettingsView"
|
||||
|
||||
export const SkillsSettings: React.FC = () => {
|
||||
const { t } = useAppTranslation()
|
||||
const { cwd, skills: rawSkills } = useExtensionState()
|
||||
const skills = useMemo(() => rawSkills ?? [], [rawSkills])
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [skillToDelete, setSkillToDelete] = useState<SkillMetadata | null>(null)
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||
|
||||
// Check if we're in a workspace/project
|
||||
const hasWorkspace = Boolean(cwd)
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
vscode.postMessage({ type: "requestSkills" })
|
||||
}, [])
|
||||
|
||||
// Request skills when component mounts
|
||||
useEffect(() => {
|
||||
handleRefresh()
|
||||
}, [handleRefresh])
|
||||
|
||||
const handleDeleteClick = useCallback((skill: SkillMetadata) => {
|
||||
setSkillToDelete(skill)
|
||||
setDeleteDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleDeleteConfirm = useCallback(() => {
|
||||
if (skillToDelete) {
|
||||
vscode.postMessage({
|
||||
type: "deleteSkill",
|
||||
skillName: skillToDelete.name,
|
||||
source: skillToDelete.source,
|
||||
skillMode: skillToDelete.mode,
|
||||
})
|
||||
setDeleteDialogOpen(false)
|
||||
setSkillToDelete(null)
|
||||
}
|
||||
}, [skillToDelete])
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setDeleteDialogOpen(false)
|
||||
setSkillToDelete(null)
|
||||
}, [])
|
||||
|
||||
const handleEditClick = useCallback((skill: SkillMetadata) => {
|
||||
vscode.postMessage({
|
||||
type: "openSkillFile",
|
||||
skillName: skill.name,
|
||||
source: skill.source,
|
||||
skillMode: skill.mode,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// No-op callback - the backend sends updated skills list via ExtensionStateContext
|
||||
const handleSkillCreated = useCallback(() => {}, [])
|
||||
|
||||
// Group skills by source
|
||||
const projectSkills = useMemo(() => skills.filter((skill) => skill.source === "project"), [skills])
|
||||
|
||||
const globalSkills = useMemo(() => skills.filter((skill) => skill.source === "global"), [skills])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader>{t("settings:sections.skills")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
{/* Description section */}
|
||||
<SearchableSetting
|
||||
settingId="skills-description"
|
||||
section={"skills" as SectionName}
|
||||
label={t("settings:sections.skills")}
|
||||
className="mb-4">
|
||||
<p className="text-sm text-vscode-descriptionForeground mb-2">
|
||||
<Trans
|
||||
i18nKey="settings:skills.description"
|
||||
components={{
|
||||
DocsLink: (
|
||||
<a
|
||||
href={buildDocLink("features/skills", "skills_settings")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-vscode-textLink-foreground hover:underline">
|
||||
Docs
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</SearchableSetting>
|
||||
|
||||
{/* Project Skills Section - Only show if in a workspace */}
|
||||
{hasWorkspace && (
|
||||
<SearchableSetting
|
||||
settingId="skills-project"
|
||||
section={"skills" as SectionName}
|
||||
label={t("settings:skills.projectSkills")}
|
||||
className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Folder className="w-3 h-3" />
|
||||
<h4 className="text-sm font-medium m-0">{t("settings:skills.projectSkills")}</h4>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="h-6 px-2 text-xs opacity-60 hover:opacity-100">
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("settings:skills.addSkill")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border border-vscode-panel-border rounded-md">
|
||||
{projectSkills.length > 0 ? (
|
||||
projectSkills.map((skill) => (
|
||||
<SkillItem
|
||||
key={`project-${skill.name}-${skill.mode || "any"}`}
|
||||
skill={skill}
|
||||
onEdit={() => handleEditClick(skill)}
|
||||
onDelete={() => handleDeleteClick(skill)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-6 text-sm text-vscode-descriptionForeground text-center">
|
||||
{t("settings:skills.noProjectSkills")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
)}
|
||||
|
||||
{/* Global Skills Section */}
|
||||
<SearchableSetting
|
||||
settingId="skills-global"
|
||||
section={"skills" as SectionName}
|
||||
label={t("settings:skills.globalSkills")}
|
||||
className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Globe className="w-3 h-3" />
|
||||
<h4 className="text-sm font-medium m-0">{t("settings:skills.globalSkills")}</h4>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="h-6 px-2 text-xs opacity-60 hover:opacity-100">
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("settings:skills.addSkill")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border border-vscode-panel-border rounded-md">
|
||||
{globalSkills.length > 0 ? (
|
||||
globalSkills.map((skill) => (
|
||||
<SkillItem
|
||||
key={`global-${skill.name}-${skill.mode || "any"}`}
|
||||
skill={skill}
|
||||
onEdit={() => handleEditClick(skill)}
|
||||
onDelete={() => handleDeleteClick(skill)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-6 text-sm text-vscode-descriptionForeground text-center">
|
||||
{t("settings:skills.noGlobalSkills")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
</Section>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("settings:skills.deleteDialog.title")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("settings:skills.deleteDialog.description", { name: skillToDelete?.name })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleDeleteCancel}>
|
||||
{t("settings:skills.deleteDialog.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm}>
|
||||
{t("settings:skills.deleteDialog.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Create Skill Dialog */}
|
||||
<CreateSkillDialog
|
||||
open={createDialogOpen}
|
||||
onOpenChange={setCreateDialogOpen}
|
||||
onSkillCreated={handleSkillCreated}
|
||||
hasWorkspace={hasWorkspace}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
import { CreateSkillDialog } from "../CreateSkillDialog"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Create a variable to hold the mock state
|
||||
let mockExtensionState: any = {}
|
||||
|
||||
// Mock the useExtensionState hook
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
ExtensionStateContextProvider: ({ children }: any) => children,
|
||||
useExtensionState: () => mockExtensionState,
|
||||
}))
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
Button: ({ children, onClick, disabled, variant }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} data-variant={variant} data-testid="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Dialog: ({ children, open }: any) => (
|
||||
<div data-testid="dialog" data-open={open}>
|
||||
{open && children}
|
||||
</div>
|
||||
),
|
||||
DialogContent: ({ children }: any) => <div data-testid="dialog-content">{children}</div>,
|
||||
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
DialogDescription: ({ children }: any) => <div data-testid="dialog-description">{children}</div>,
|
||||
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
||||
Select: ({ children, value, onValueChange }: any) => (
|
||||
<div data-testid="select" data-value={value}>
|
||||
{children}
|
||||
<input
|
||||
type="hidden"
|
||||
data-testid="select-input"
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: any) => <div data-testid="select-trigger">{children}</div>,
|
||||
SelectValue: ({ placeholder }: any) => <span data-testid="select-value">{placeholder}</span>,
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div data-testid={`select-item-${value}`} data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("CreateSkillDialog", () => {
|
||||
const mockOnOpenChange = vi.fn()
|
||||
const mockOnSkillCreated = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockExtensionState = {
|
||||
customModes: [{ slug: "custom-mode", name: "Custom Mode" }],
|
||||
}
|
||||
})
|
||||
|
||||
it("renders dialog when open is true", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("dialog")).toHaveAttribute("data-open", "true")
|
||||
expect(screen.getByTestId("dialog-title")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render dialog content when open is false", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={false}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("dialog")).toHaveAttribute("data-open", "false")
|
||||
expect(screen.queryByTestId("dialog-title")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders name input field", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
expect(nameInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders description textarea", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
|
||||
expect(descInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("transforms name input to lowercase with only allowed characters", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"settings:skills.createDialog.namePlaceholder",
|
||||
) as HTMLInputElement
|
||||
fireEvent.change(nameInput, { target: { value: "Test-Skill_123!" } })
|
||||
|
||||
// Should be transformed to lowercase and remove invalid characters
|
||||
expect(nameInput.value).toBe("test-skill123")
|
||||
})
|
||||
|
||||
it("disables create button when name is empty", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
expect(createButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("disables create button when description is empty", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
fireEvent.change(nameInput, { target: { value: "valid-name" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
expect(createButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("enables create button when both name and description are provided", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "valid-name" } })
|
||||
fireEvent.change(descInput, { target: { value: "Valid description" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
expect(createButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it("calls vscode.postMessage with correct data when creating skill", async () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "my-skill" } })
|
||||
fireEvent.change(descInput, { target: { value: "My skill description" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
fireEvent.click(createButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "createSkill",
|
||||
skillName: "my-skill",
|
||||
source: "project",
|
||||
skillDescription: "My skill description",
|
||||
skillMode: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("calls onSkillCreated after creating skill", async () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "my-skill" } })
|
||||
fireEvent.change(descInput, { target: { value: "My skill description" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
fireEvent.click(createButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSkillCreated).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("closes dialog after creating skill", async () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
|
||||
const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "my-skill" } })
|
||||
fireEvent.change(descInput, { target: { value: "My skill description" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
fireEvent.click(createButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("closes dialog when cancel button is clicked", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const cancelButton = buttons.find((btn) => btn.getAttribute("data-variant") === "secondary")
|
||||
|
||||
fireEvent.click(cancelButton!)
|
||||
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it("defaults to project source when hasWorkspace is true", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getAllByTestId("select")[0]
|
||||
expect(select).toHaveAttribute("data-value", "project")
|
||||
})
|
||||
|
||||
it("defaults to global source when hasWorkspace is false", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getAllByTestId("select")[0]
|
||||
expect(select).toHaveAttribute("data-value", "global")
|
||||
})
|
||||
|
||||
it("renders source selection dropdown", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("select-item-global")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("select-item-project")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode selection dropdown", () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should have "Any mode" option (uses __any__ sentinel value)
|
||||
expect(screen.getByTestId("select-item-__any__")).toBeInTheDocument()
|
||||
// Should have built-in modes
|
||||
expect(screen.getByTestId("select-item-code")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("select-item-architect")).toBeInTheDocument()
|
||||
// Should have custom modes from state
|
||||
expect(screen.getByTestId("select-item-custom-mode")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("clears form after successful skill creation", async () => {
|
||||
render(
|
||||
<CreateSkillDialog
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSkillCreated={mockOnSkillCreated}
|
||||
hasWorkspace={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"settings:skills.createDialog.namePlaceholder",
|
||||
) as HTMLInputElement
|
||||
const descInput = screen.getByPlaceholderText(
|
||||
"settings:skills.createDialog.descriptionPlaceholder",
|
||||
) as HTMLTextAreaElement
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "test-skill" } })
|
||||
fireEvent.change(descInput, { target: { value: "Test description" } })
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const createButton = buttons.find((btn) => btn.getAttribute("data-variant") === "primary")
|
||||
|
||||
fireEvent.click(createButton!)
|
||||
|
||||
// After clicking create, the dialog should close via onOpenChange
|
||||
await waitFor(() => {
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -54,6 +54,31 @@ vi.mock("@src/components/ui", () => ({
|
|||
TooltipProvider: ({ children }: any) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: any) => <>{children}</>,
|
||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
||||
// Add Dialog components (used by CreateSkillDialog)
|
||||
Dialog: ({ children, open }: any) => (open ? <div data-testid="dialog">{children}</div> : null),
|
||||
DialogContent: ({ children, className }: any) => (
|
||||
<div data-testid="dialog-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
DialogDescription: ({ children }: any) => <div data-testid="dialog-description">{children}</div>,
|
||||
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
||||
// Add Select components (used by CreateSkillDialog)
|
||||
Select: ({ children, value, onValueChange: _onValueChange }: any) => (
|
||||
<div data-testid="select" data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div data-testid={`select-item-${value}`} data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: any) => <div data-testid="select-trigger">{children}</div>,
|
||||
SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
|
||||
}))
|
||||
|
||||
// Mock ModesView and McpView since they're rendered during indexing
|
||||
|
|
|
|||
|
|
@ -212,6 +212,17 @@ vi.mock("@/components/ui", () => ({
|
|||
CollapsibleContent: ({ children, className }: any) => (
|
||||
<div className={`collapsible-content-mock ${className || ""}`}>{children}</div>
|
||||
),
|
||||
// Add Dialog components (used by CreateSkillDialog)
|
||||
Dialog: ({ children, open }: any) => (open ? <div data-testid="dialog">{children}</div> : null),
|
||||
DialogContent: ({ children, className }: any) => (
|
||||
<div data-testid="dialog-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
DialogDescription: ({ children }: any) => <div data-testid="dialog-description">{children}</div>,
|
||||
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
|
|
|
|||
|
|
@ -55,6 +55,31 @@ vi.mock("@src/components/ui", () => ({
|
|||
Popover: ({ children }: any) => <>{children}</>,
|
||||
PopoverTrigger: ({ children }: any) => <>{children}</>,
|
||||
PopoverContent: ({ children }: any) => <div>{children}</div>,
|
||||
// Add Dialog components (used by CreateSkillDialog)
|
||||
Dialog: ({ children, open }: any) => (open ? <div data-testid="dialog">{children}</div> : null),
|
||||
DialogContent: ({ children, className }: any) => (
|
||||
<div data-testid="dialog-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
DialogDescription: ({ children }: any) => <div data-testid="dialog-description">{children}</div>,
|
||||
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
||||
// Add Select components (used by CreateSkillDialog)
|
||||
Select: ({ children, value, onValueChange: _onValueChange }: any) => (
|
||||
<div data-testid="select" data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div data-testid={`select-item-${value}`} data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: any) => <div data-testid="select-trigger">{children}</div>,
|
||||
SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
|
||||
}))
|
||||
|
||||
// Mock ModesView and McpView since they're rendered during indexing
|
||||
|
|
|
|||
141
webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx
Normal file
141
webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { render, screen, fireEvent } from "@/utils/test-utils"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { SkillItem } from "../SkillItem"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
Button: ({ children, onClick, className, tabIndex }: any) => (
|
||||
<button onClick={onClick} className={className} tabIndex={tabIndex} data-testid="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
StandardTooltip: ({ children, content }: any) => (
|
||||
<div title={content} data-testid="tooltip">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
describe("SkillItem", () => {
|
||||
const mockOnEdit = vi.fn()
|
||||
const mockOnDelete = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders skill name", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
expect(screen.getByText("test-skill")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders skill description", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
expect(screen.getByText("A test skill description")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode badge when skill has mode", () => {
|
||||
render(<SkillItem skill={mockSkillWithMode} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
expect(screen.getByText("architect")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render mode badge when skill has no mode", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Should not have any mode badge
|
||||
const container = screen.getByText("test-skill").parentElement
|
||||
expect(container?.querySelector(".bg-vscode-badge-background")).toBeNull()
|
||||
})
|
||||
|
||||
it("calls onEdit when edit button is clicked", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
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", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
// Second button is delete
|
||||
fireEvent.click(buttons[1])
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onEdit when clicking on skill name area", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const nameElement = screen.getByText("test-skill")
|
||||
fireEvent.click(nameElement)
|
||||
|
||||
expect(mockOnEdit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("renders without description when not provided", () => {
|
||||
const skillWithoutDescription: SkillMetadata = {
|
||||
name: "no-desc-skill",
|
||||
description: "",
|
||||
path: "/path/to/skill/SKILL.md",
|
||||
source: "project",
|
||||
}
|
||||
|
||||
render(<SkillItem skill={skillWithoutDescription} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
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(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const itemDiv = container.firstChild
|
||||
expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground")
|
||||
})
|
||||
|
||||
it("renders both edit and delete buttons", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
expect(buttons).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
import { SkillsSettings } from "../SkillsSettings"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (params?.name) {
|
||||
return `${key} ${params.name}`
|
||||
}
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the doc links utility
|
||||
vi.mock("@/utils/docLinks", () => ({
|
||||
buildDocLink: (path: string, anchor?: string) => `https://docs.example.com/${path}${anchor ? `#${anchor}` : ""}`,
|
||||
}))
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
AlertDialog: ({ children, open }: any) => (
|
||||
<div data-testid="alert-dialog" data-open={open}>
|
||||
{open && children}
|
||||
</div>
|
||||
),
|
||||
AlertDialogContent: ({ children }: any) => <div data-testid="alert-dialog-content">{children}</div>,
|
||||
AlertDialogHeader: ({ children }: any) => <div data-testid="alert-dialog-header">{children}</div>,
|
||||
AlertDialogTitle: ({ children }: any) => <div data-testid="alert-dialog-title">{children}</div>,
|
||||
AlertDialogDescription: ({ children }: any) => <div data-testid="alert-dialog-description">{children}</div>,
|
||||
AlertDialogFooter: ({ children }: any) => <div data-testid="alert-dialog-footer">{children}</div>,
|
||||
AlertDialogAction: ({ children, onClick }: any) => (
|
||||
<button data-testid="alert-dialog-action" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogCancel: ({ children, onClick }: any) => (
|
||||
<button data-testid="alert-dialog-cancel" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Button: ({ children, onClick, disabled, className, variant, size }: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-testid="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock SkillItem component
|
||||
vi.mock("../SkillItem", () => ({
|
||||
SkillItem: ({ skill, onEdit, onDelete }: any) => (
|
||||
<div data-testid={`skill-item-${skill.name}`}>
|
||||
<span>{skill.name}</span>
|
||||
{skill.description && <span>{skill.description}</span>}
|
||||
{skill.mode && <span data-testid={`skill-mode-${skill.name}`}>{skill.mode}</span>}
|
||||
<button onClick={onEdit} data-testid={`edit-${skill.name}`}>
|
||||
Edit
|
||||
</button>
|
||||
<button onClick={onDelete} data-testid={`delete-${skill.name}`}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock CreateSkillDialog component
|
||||
vi.mock("../CreateSkillDialog", () => ({
|
||||
CreateSkillDialog: ({ open, onOpenChange, onSkillCreated }: any) => (
|
||||
<div data-testid="create-skill-dialog" data-open={open}>
|
||||
{open && (
|
||||
<>
|
||||
<button onClick={() => onOpenChange(false)} data-testid="close-dialog">
|
||||
Close
|
||||
</button>
|
||||
<button onClick={onSkillCreated} data-testid="create-skill-button">
|
||||
Create
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock SectionHeader and Section components
|
||||
vi.mock("../SectionHeader", () => ({
|
||||
SectionHeader: ({ children }: any) => <div data-testid="section-header">{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock("../Section", () => ({
|
||||
Section: ({ children }: any) => <div data-testid="section">{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock SearchableSetting
|
||||
vi.mock("../SearchableSetting", () => ({
|
||||
SearchableSetting: ({ children }: any) => <div data-testid="searchable-setting">{children}</div>,
|
||||
}))
|
||||
|
||||
const mockSkills: SkillMetadata[] = [
|
||||
{
|
||||
name: "project-skill",
|
||||
description: "A project skill",
|
||||
path: "/workspace/.roo/skills/project-skill/SKILL.md",
|
||||
source: "project",
|
||||
},
|
||||
{
|
||||
name: "project-mode-skill",
|
||||
description: "A project mode-specific skill",
|
||||
path: "/workspace/.roo/skills-architect/project-mode-skill/SKILL.md",
|
||||
source: "project",
|
||||
mode: "architect",
|
||||
},
|
||||
{
|
||||
name: "global-skill",
|
||||
description: "A global skill",
|
||||
path: "/home/.roo/skills/global-skill/SKILL.md",
|
||||
source: "global",
|
||||
},
|
||||
]
|
||||
|
||||
// Create a variable to hold the mock state
|
||||
let mockExtensionState: any = {}
|
||||
|
||||
// Mock the useExtensionState hook
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
ExtensionStateContextProvider: ({ children }: any) => children,
|
||||
useExtensionState: () => mockExtensionState,
|
||||
}))
|
||||
|
||||
const renderSkillsSettings = (skills: SkillMetadata[] = mockSkills, cwd?: string) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
// Update the mock state before rendering
|
||||
mockExtensionState = {
|
||||
skills,
|
||||
cwd: cwd !== undefined ? cwd : "/workspace",
|
||||
customModes: [],
|
||||
}
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ExtensionStateContextProvider>
|
||||
<SkillsSettings />
|
||||
</ExtensionStateContextProvider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("SkillsSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders section header", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
expect(screen.getByTestId("section-header")).toBeInTheDocument()
|
||||
expect(screen.getByText("settings:sections.skills")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("requests skills on mount", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "requestSkills" })
|
||||
})
|
||||
|
||||
it("displays project skills section when in a workspace", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
expect(screen.getByText("settings:skills.projectSkills")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("skill-item-project-skill")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays global skills section", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
expect(screen.getByText("settings:skills.globalSkills")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("skill-item-global-skill")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not display project skills section when not in a workspace", () => {
|
||||
const globalOnlySkills = mockSkills.filter((s) => s.source === "global")
|
||||
renderSkillsSettings(globalOnlySkills, "")
|
||||
|
||||
expect(screen.queryByText("settings:skills.projectSkills")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows empty state for project skills when none exist", () => {
|
||||
const globalOnlySkills = mockSkills.filter((s) => s.source === "global")
|
||||
renderSkillsSettings(globalOnlySkills)
|
||||
|
||||
expect(screen.getByText("settings:skills.noProjectSkills")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows empty state for global skills when none exist", () => {
|
||||
const projectOnlySkills = mockSkills.filter((s) => s.source === "project")
|
||||
renderSkillsSettings(projectOnlySkills)
|
||||
|
||||
expect(screen.getByText("settings:skills.noGlobalSkills")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("groups skills by source correctly", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
// Project skills
|
||||
expect(screen.getByTestId("skill-item-project-skill")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("skill-item-project-mode-skill")).toBeInTheDocument()
|
||||
|
||||
// Global skills
|
||||
expect(screen.getByTestId("skill-item-global-skill")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays mode badge for mode-specific skills", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
expect(screen.getByTestId("skill-mode-project-mode-skill")).toBeInTheDocument()
|
||||
expect(screen.getByText("architect")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("opens create skill dialog when add button is clicked", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const addButtons = screen.getAllByTestId("button")
|
||||
fireEvent.click(addButtons[0])
|
||||
|
||||
expect(screen.getByTestId("create-skill-dialog")).toHaveAttribute("data-open", "true")
|
||||
})
|
||||
|
||||
it("opens delete confirmation dialog when delete button is clicked", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-project-skill")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
expect(screen.getByTestId("alert-dialog")).toHaveAttribute("data-open", "true")
|
||||
expect(screen.getByText("settings:skills.deleteDialog.title")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("deletes skill when confirmation is clicked", async () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-project-skill")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
const confirmButton = screen.getByTestId("alert-dialog-action")
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteSkill",
|
||||
skillName: "project-skill",
|
||||
source: "project",
|
||||
skillMode: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("cancels deletion when cancel is clicked", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-project-skill")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
const cancelButton = screen.getByTestId("alert-dialog-cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(screen.getByTestId("alert-dialog")).toHaveAttribute("data-open", "false")
|
||||
})
|
||||
|
||||
it("opens skill file when edit button is clicked", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const editButton = screen.getByTestId("edit-project-skill")
|
||||
fireEvent.click(editButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "openSkillFile",
|
||||
skillName: "project-skill",
|
||||
source: "project",
|
||||
skillMode: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("sends mode when editing mode-specific skill", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const editButton = screen.getByTestId("edit-project-mode-skill")
|
||||
fireEvent.click(editButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "openSkillFile",
|
||||
skillName: "project-mode-skill",
|
||||
source: "project",
|
||||
skillMode: "architect",
|
||||
})
|
||||
})
|
||||
|
||||
it("sends mode when deleting mode-specific skill", async () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-project-mode-skill")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
const confirmButton = screen.getByTestId("alert-dialog-action")
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteSkill",
|
||||
skillName: "project-mode-skill",
|
||||
source: "project",
|
||||
skillMode: "architect",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("does not manually refresh after deletion (backend sends updated skills via context)", async () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
// Clear mock calls after initial mount
|
||||
;(vscode.postMessage as any).mockClear()
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-project-skill")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
const confirmButton = screen.getByTestId("alert-dialog-action")
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Verify deleteSkill message was sent
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteSkill",
|
||||
skillName: "project-skill",
|
||||
source: "project",
|
||||
skillMode: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
// Verify that requestSkills was NOT called after deletion
|
||||
// (the backend sends updated skills via ExtensionStateContext automatically)
|
||||
const calls = (vscode.postMessage as any).mock.calls
|
||||
const refreshCalls = calls.filter((call: any[]) => call[0].type === "requestSkills")
|
||||
expect(refreshCalls.length).toBe(0)
|
||||
})
|
||||
|
||||
it("does not manually refresh after creating new skill (backend sends updated skills via context)", async () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
// Clear mock calls after initial mount
|
||||
;(vscode.postMessage as any).mockClear()
|
||||
|
||||
// Open create dialog
|
||||
const addButtons = screen.getAllByTestId("button")
|
||||
fireEvent.click(addButtons[0])
|
||||
|
||||
// Simulate skill creation
|
||||
const createButton = screen.getByTestId("create-skill-button")
|
||||
fireEvent.click(createButton)
|
||||
|
||||
// Verify that requestSkills was NOT called after creation
|
||||
// (the backend sends updated skills via ExtensionStateContext automatically)
|
||||
const calls = (vscode.postMessage as any).mock.calls
|
||||
const refreshCalls = calls.filter((call: any[]) => call[0].type === "requestSkills")
|
||||
expect(refreshCalls.length).toBe(0)
|
||||
})
|
||||
|
||||
it("renders empty state when no skills exist", () => {
|
||||
renderSkillsSettings([])
|
||||
|
||||
expect(screen.getByText("settings:skills.noProjectSkills")).toBeInTheDocument()
|
||||
expect(screen.getByText("settings:skills.noGlobalSkills")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("handles multiple skills of the same source", () => {
|
||||
const multipleSkills: SkillMetadata[] = [
|
||||
{
|
||||
name: "skill-1",
|
||||
description: "First skill",
|
||||
path: "/path/1",
|
||||
source: "global",
|
||||
},
|
||||
{
|
||||
name: "skill-2",
|
||||
description: "Second skill",
|
||||
path: "/path/2",
|
||||
source: "global",
|
||||
},
|
||||
{
|
||||
name: "skill-3",
|
||||
description: "Third skill",
|
||||
path: "/path/3",
|
||||
source: "global",
|
||||
},
|
||||
]
|
||||
|
||||
renderSkillsSettings(multipleSkills)
|
||||
|
||||
expect(screen.getByTestId("skill-item-skill-1")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("skill-item-skill-2")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("skill-item-skill-3")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders add skill button in each section", () => {
|
||||
renderSkillsSettings()
|
||||
|
||||
// Should have two "Add Skill" buttons - one for project, one for global
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
const addButtons = buttons.filter((btn) => btn.textContent?.includes("settings:skills.addSkill"))
|
||||
expect(addButtons.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
type MarketplaceInstalledMetadata,
|
||||
type Command,
|
||||
type McpServer,
|
||||
type SkillMetadata,
|
||||
RouterModels,
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
|
|
@ -42,6 +43,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
filePaths: string[]
|
||||
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
|
||||
commands: Command[]
|
||||
skills: SkillMetadata[]
|
||||
organizationAllowList: OrganizationAllowList
|
||||
organizationSettingsVersion: number
|
||||
cloudIsAuthenticated: boolean
|
||||
|
|
@ -278,6 +280,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [filePaths, setFilePaths] = useState<string[]>([])
|
||||
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
|
||||
const [commands, setCommands] = useState<Command[]>([])
|
||||
const [skills, setSkills] = useState<SkillMetadata[]>([])
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
|
||||
const [extensionRouterModels, setExtensionRouterModels] = useState<RouterModels | undefined>(undefined)
|
||||
|
|
@ -376,6 +379,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setCommands(message.commands ?? [])
|
||||
break
|
||||
}
|
||||
case "skills": {
|
||||
setSkills(message.skills ?? [])
|
||||
break
|
||||
}
|
||||
case "messageUpdated": {
|
||||
const clineMessage = message.clineMessage!
|
||||
setState((prevState) => {
|
||||
|
|
@ -488,6 +495,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
filePaths,
|
||||
openedTabs,
|
||||
commands,
|
||||
skills,
|
||||
soundVolume: state.soundVolume,
|
||||
ttsSpeed: state.ttsSpeed,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/ca/settings.json
generated
48
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Experimental",
|
||||
"language": "Idioma",
|
||||
"about": "Sobre Roo Code"
|
||||
"about": "Sobre Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -996,5 +997,50 @@
|
|||
"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. <DocsLink>Més informació</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/de/settings.json
generated
48
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Experimentell",
|
||||
"language": "Sprache",
|
||||
"about": "Über Roo Code"
|
||||
"about": "Über Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -996,5 +997,50 @@
|
|||
"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. <DocsLink>Mehr erfahren</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"modes": "Modes",
|
||||
"mcp": "MCP Servers",
|
||||
"worktrees": "Worktrees",
|
||||
"skills": "Skills",
|
||||
"autoApprove": "Auto-Approve",
|
||||
"browser": "Browser",
|
||||
"checkpoints": "Checkpoints",
|
||||
|
|
@ -70,6 +71,51 @@
|
|||
"slashCommands": {
|
||||
"description": "Manage your slash commands to quickly execute custom workflows and actions. <DocsLink>Learn more</DocsLink>"
|
||||
},
|
||||
"skills": {
|
||||
"description": "Manage skills that provide contextual instructions to the agent. Skills are automatically applied when relevant to your tasks. <DocsLink>Learn more</DocsLink>",
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/es/settings.json
generated
48
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Experimental",
|
||||
"language": "Idioma",
|
||||
"about": "Acerca de Roo Code"
|
||||
"about": "Acerca de Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -996,5 +997,50 @@
|
|||
"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. <DocsLink>Más información</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/fr/settings.json
generated
48
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -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": {
|
||||
|
|
@ -996,5 +997,50 @@
|
|||
"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. <DocsLink>En savoir plus</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/hi/settings.json
generated
48
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "प्रायोगिक",
|
||||
"language": "भाषा",
|
||||
"about": "परिचय"
|
||||
"about": "परिचय",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"label": "संदेश भेजने के लिए {{primaryMod}}+Enter की आवश्यकता है",
|
||||
"description": "जब सक्षम हो, तो आपको केवल Enter के बजाय संदेश भेजने के लिए {{primaryMod}}+Enter दबाना होगा"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "Skills का प्रबंधन करें जो एजेंट को संदर्भात्मक निर्देश प्रदान करते हैं। जब आपके कार्यों के लिए प्रासंगिक हों तो Skills स्वचालित रूप से लागू होते हैं। <DocsLink>और जानें</DocsLink>",
|
||||
"projectSkills": "प्रोजेक्ट Skills",
|
||||
"globalSkills": "ग्लोबल Skills",
|
||||
"noProjectSkills": "कोई प्रोजेक्ट skills कॉन्फ़िगर नहीं किया गया। प्रोजेक्ट-विशिष्ट एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।",
|
||||
"noGlobalSkills": "कोई ग्लोबल skills कॉन्फ़िगर नहीं किया गया। सभी प्रोजेक्ट्स में उपलब्ध एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।",
|
||||
"addSkill": "Skill जोड़ें",
|
||||
"editSkill": "Skill संपादित करें",
|
||||
"deleteSkill": "Skill हटाएं",
|
||||
"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 वर्णों से अधिक नहीं होना चाहिए"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/id/settings.json
generated
48
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Eksperimental",
|
||||
"language": "Bahasa",
|
||||
"about": "Tentang Roo Code"
|
||||
"about": "Tentang Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -1026,5 +1027,50 @@
|
|||
"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. <DocsLink>Pelajari lebih lanjut</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/it/settings.json
generated
48
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Sperimentale",
|
||||
"language": "Lingua",
|
||||
"about": "Informazioni su Roo Code"
|
||||
"about": "Informazioni su Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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à. <DocsLink>Scopri di più</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/ja/settings.json
generated
48
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "実験的",
|
||||
"language": "言語",
|
||||
"about": "Roo Codeについて"
|
||||
"about": "Roo Codeについて",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"label": "メッセージを送信するには{{primaryMod}}+Enterが必要",
|
||||
"description": "有効にすると、Enterだけでなく{{primaryMod}}+Enterを押してメッセージを送信する必要があります"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "エージェントにコンテキスト指示を提供するスキルを管理します。スキルはタスクに関連する場合に自動的に適用されます。<DocsLink>詳細を見る</DocsLink>",
|
||||
"projectSkills": "プロジェクトスキル",
|
||||
"globalSkills": "グローバルスキル",
|
||||
"noProjectSkills": "プロジェクトスキルが設定されていません。プロジェクト固有のエージェント機能を追加するには、作成してください。",
|
||||
"noGlobalSkills": "グローバルスキルが設定されていません。すべてのプロジェクトで利用可能なエージェント機能を追加するには、作成してください。",
|
||||
"addSkill": "スキルを追加",
|
||||
"editSkill": "スキルを編集",
|
||||
"deleteSkill": "スキルを削除",
|
||||
"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文字以内である必要があります"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/ko/settings.json
generated
48
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "실험적",
|
||||
"language": "언어",
|
||||
"about": "Roo Code 정보"
|
||||
"about": "Roo Code 정보",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"label": "메시지를 보내려면 {{primaryMod}}+Enter가 필요",
|
||||
"description": "활성화하면 Enter만으로는 안 되고 {{primaryMod}}+Enter를 눌러야 메시지를 보낼 수 있습니다"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "에이전트에 컨텍스트 지침을 제공하는 스킬을 관리합니다. 스킬은 작업과 관련이 있을 때 자동으로 적용됩니다. <DocsLink>자세히 알아보기</DocsLink>",
|
||||
"projectSkills": "프로젝트 스킬",
|
||||
"globalSkills": "전역 스킬",
|
||||
"noProjectSkills": "구성된 프로젝트 스킬이 없습니다. 프로젝트별 에이전트 기능을 추가하려면 하나를 만드세요.",
|
||||
"noGlobalSkills": "구성된 전역 스킬이 없습니다. 모든 프로젝트에서 사용할 수 있는 에이전트 기능을 추가하려면 하나를 만드세요.",
|
||||
"addSkill": "스킬 추가",
|
||||
"editSkill": "스킬 편집",
|
||||
"deleteSkill": "스킬 삭제",
|
||||
"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자 이하여야 합니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/nl/settings.json
generated
48
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Experimenteel",
|
||||
"language": "Taal",
|
||||
"about": "Over Roo Code"
|
||||
"about": "Over Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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. <DocsLink>Meer informatie</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/pl/settings.json
generated
48
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Eksperymentalne",
|
||||
"language": "Język",
|
||||
"about": "O Roo Code"
|
||||
"about": "O Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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ń. <DocsLink>Dowiedz się więcej</DocsLink>",
|
||||
"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ść",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
48
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Experimental",
|
||||
"language": "Idioma",
|
||||
"about": "Sobre"
|
||||
"about": "Sobre",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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. <DocsLink>Saiba mais</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/ru/settings.json
generated
48
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Экспериментальное",
|
||||
"language": "Язык",
|
||||
"about": "О Roo Code"
|
||||
"about": "О Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"label": "Требовать {{primaryMod}}+Enter для отправки сообщений",
|
||||
"description": "Если включено, необходимо нажать {{primaryMod}}+Enter для отправки сообщений вместо простого Enter"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "Управляйте навыками, которые предоставляют контекстные инструкции агенту. Навыки автоматически применяются, когда они релевантны вашим задачам. <DocsLink>Узнать больше</DocsLink>",
|
||||
"projectSkills": "Навыки Проекта",
|
||||
"globalSkills": "Глобальные Навыки",
|
||||
"noProjectSkills": "Навыки проекта не настроены. Создайте навык, чтобы добавить возможности агента для конкретного проекта.",
|
||||
"noGlobalSkills": "Глобальные навыки не настроены. Создайте навык, чтобы добавить возможности агента, доступные во всех проектах.",
|
||||
"addSkill": "Добавить Навык",
|
||||
"editSkill": "Редактировать навык",
|
||||
"deleteSkill": "Удалить навык",
|
||||
"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 символов"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/tr/settings.json
generated
48
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "Deneysel",
|
||||
"language": "Dil",
|
||||
"about": "Roo Code Hakkında"
|
||||
"about": "Roo Code Hakkında",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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. <DocsLink>Daha fazla bilgi</DocsLink>",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/vi/settings.json
generated
48
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -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": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"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. <DocsLink>Tìm hiểu thêm</DocsLink>",
|
||||
"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",
|
||||
"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ự"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
48
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "实验性",
|
||||
"language": "语言",
|
||||
"about": "关于 Roo Code"
|
||||
"about": "关于 Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -997,5 +998,50 @@
|
|||
"label": "需要 {{primaryMod}}+Enter 发送消息",
|
||||
"description": "启用后,必须按 {{primaryMod}}+Enter 发送消息,而不仅仅是 Enter"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "管理为代理提供上下文指令的技能。技能会在与您的任务相关时自动应用。<DocsLink>了解更多</DocsLink>",
|
||||
"projectSkills": "项目技能",
|
||||
"globalSkills": "全局技能",
|
||||
"noProjectSkills": "未配置项目技能。创建一个以添加特定于项目的代理功能。",
|
||||
"noGlobalSkills": "未配置全局技能。创建一个以添加在所有项目中可用的代理功能。",
|
||||
"addSkill": "添加技能",
|
||||
"editSkill": "编辑技能",
|
||||
"deleteSkill": "删除技能",
|
||||
"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个字符"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
58
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -41,7 +41,8 @@
|
|||
"ui": "UI",
|
||||
"experimental": "實驗性",
|
||||
"language": "語言",
|
||||
"about": "關於 Roo Code"
|
||||
"about": "關於 Roo Code",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"about": {
|
||||
"bugReport": {
|
||||
|
|
@ -994,5 +995,60 @@
|
|||
"output": "輸出",
|
||||
"cacheReads": "快取讀取"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"collapseThinking": {
|
||||
"label": "預設折疊「思考」訊息",
|
||||
"description": "啟用後,「思考」塊將預設折疊,直到您與其互動"
|
||||
},
|
||||
"requireCtrlEnterToSend": {
|
||||
"label": "需要 {{primaryMod}}+Enter 傳送訊息",
|
||||
"description": "啟用後,必須按 {{primaryMod}}+Enter 傳送訊息,而不只是 Enter"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "管理為代理提供上下文指令的技能。技能會在與您的任務相關時自動套用。<DocsLink>深入了解</DocsLink>",
|
||||
"projectSkills": "專案技能",
|
||||
"globalSkills": "全域技能",
|
||||
"noProjectSkills": "未設定專案技能。建立一個以新增專案特定的代理功能。",
|
||||
"noGlobalSkills": "未設定全域技能。建立一個以新增在所有專案中可用的代理功能。",
|
||||
"addSkill": "新增技能",
|
||||
"editSkill": "編輯技能",
|
||||
"deleteSkill": "刪除技能",
|
||||
"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個字元"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue