diff --git a/apps/cli/src/ui/components/tools/types.ts b/apps/cli/src/ui/components/tools/types.ts
index 28a1b5faa02..a16fbd60ea3 100644
--- a/apps/cli/src/ui/components/tools/types.ts
+++ b/apps/cli/src/ui/components/tools/types.ts
@@ -16,15 +16,7 @@ export type ToolCategory =
| "other"
export function getToolCategory(toolName: string): ToolCategory {
- const fileReadTools = [
- "readFile",
- "read_file",
- "fetchInstructions",
- "fetch_instructions",
- "listFilesTopLevel",
- "listFilesRecursive",
- "list_files",
- ]
+ const fileReadTools = ["readFile", "read_file", "skill", "listFilesTopLevel", "listFilesRecursive", "list_files"]
const fileWriteTools = [
"editedExistingFile",
diff --git a/apps/cli/src/ui/components/tools/utils.ts b/apps/cli/src/ui/components/tools/utils.ts
index 5eaee33b127..31acf2cccbc 100644
--- a/apps/cli/src/ui/components/tools/utils.ts
+++ b/apps/cli/src/ui/components/tools/utils.ts
@@ -50,8 +50,7 @@ export function getToolDisplayName(toolName: string): string {
// File read operations
readFile: "Read",
read_file: "Read",
- fetchInstructions: "Fetch Instructions",
- fetch_instructions: "Fetch Instructions",
+ skill: "Load Skill",
listFilesTopLevel: "List Files",
listFilesRecursive: "List Files (Recursive)",
list_files: "List Files",
@@ -107,8 +106,7 @@ export function getToolIconName(toolName: string): IconName {
// File read operations
readFile: "file",
read_file: "file",
- fetchInstructions: "file",
- fetch_instructions: "file",
+ skill: "file",
listFilesTopLevel: "folder",
listFilesRecursive: "folder",
list_files: "folder",
diff --git a/apps/web-roo-code/src/app/linear/page.tsx b/apps/web-roo-code/src/app/linear/page.tsx
deleted file mode 100644
index 40334e2698a..00000000000
--- a/apps/web-roo-code/src/app/linear/page.tsx
+++ /dev/null
@@ -1,413 +0,0 @@
-import {
- ArrowRight,
- CheckCircle,
- CreditCard,
- Eye,
- GitBranch,
- GitPullRequest,
- Link2,
- MessageSquare,
- Settings,
- Shield,
-} from "lucide-react"
-import type { LucideIcon } from "lucide-react"
-import type { Metadata } from "next"
-
-import { AnimatedBackground } from "@/components/homepage"
-import { LinearIssueDemo } from "@/components/linear/linear-issue-demo"
-import { Button } from "@/components/ui"
-import { EXTERNAL_LINKS } from "@/lib/constants"
-import { SEO } from "@/lib/seo"
-import { ogImageUrl } from "@/lib/og"
-
-const TITLE = "Roo Code for Linear"
-const DESCRIPTION = "Assign development work to @Roo Code directly from Linear. Get PRs back without switching tools."
-const OG_DESCRIPTION = "Turn Linear Issues into Pull Requests"
-const PATH = "/linear"
-
-// Featured Workflow section is temporarily commented out until video is ready
-// const LINEAR_DEMO_YOUTUBE_ID = ""
-
-export const metadata: Metadata = {
- title: TITLE,
- description: DESCRIPTION,
- alternates: {
- canonical: `${SEO.url}${PATH}`,
- },
- openGraph: {
- title: TITLE,
- description: DESCRIPTION,
- url: `${SEO.url}${PATH}`,
- siteName: SEO.name,
- images: [
- {
- url: ogImageUrl(TITLE, OG_DESCRIPTION),
- width: 1200,
- height: 630,
- alt: TITLE,
- },
- ],
- locale: SEO.locale,
- type: "website",
- },
- twitter: {
- card: SEO.twitterCard,
- title: TITLE,
- description: DESCRIPTION,
- images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
- },
- keywords: [
- ...SEO.keywords,
- "linear integration",
- "issue to PR",
- "AI in Linear",
- "engineering workflow automation",
- "Roo Code Cloud",
- ],
-}
-
-// Invalidate cache when a request comes in, at most once every hour.
-export const revalidate = 3600
-
-type ValueProp = {
- icon: LucideIcon
- title: string
- description: string
-}
-
-const VALUE_PROPS: ValueProp[] = [
- {
- icon: GitBranch,
- title: "Work where you already work.",
- description:
- "Assign development work to @Roo Code directly from Linear. No new tools to learn, no context switching required.",
- },
- {
- icon: Eye,
- title: "Progress is visible.",
- description:
- "Watch progress unfold in real-time. Roo Code posts updates as comments, so your whole team stays in the loop.",
- },
- {
- icon: MessageSquare,
- title: "Mention for refinement.",
- description:
- 'Need changes? Just comment "@Roo Code also add dark mode support" and the agent picks up where it left off.',
- },
- {
- icon: Link2,
- title: "Full traceability.",
- description:
- "Every PR links back to the originating issue. Every issue shows its linked PR. Your audit trail stays clean.",
- },
- {
- icon: Settings,
- title: "Organization-level setup.",
- description:
- "Connect once, use everywhere. Your team members can assign issues to @Roo Code without individual configuration.",
- },
- {
- icon: Shield,
- title: "Safe by design.",
- description:
- "Agents never touch main/master directly. They produce branches and PRs. You review and approve before merge.",
- },
-]
-
-// type WorkflowStep = {
-// step: number
-// title: string
-// description: string
-// }
-
-// const WORKFLOW_STEPS: WorkflowStep[] = [
-// {
-// step: 1,
-// title: "Create an issue",
-// description: "Write your issue with acceptance criteria. Be as detailed as you like.",
-// },
-// {
-// step: 2,
-// title: "Call @Roo Code",
-// description: "Mention @Roo Code in a comment to start. The agent begins working immediately.",
-// },
-// {
-// step: 3,
-// title: "Watch progress",
-// description: "Roo Code posts status updates as comments. Refine with @-mentions if needed.",
-// },
-// {
-// step: 4,
-// title: "Review the PR",
-// description: "When ready, the PR link appears in the issue. Review, iterate, and ship.",
-// },
-// ]
-
-type OnboardingStep = {
- icon: LucideIcon
- title: string
- description: string
- link?: {
- href: string
- text: string
- }
-}
-
-const ONBOARDING_STEPS: OnboardingStep[] = [
- {
- icon: CreditCard,
- title: "1. Team Plan",
- description: "Linear integration requires a Team plan.",
- link: {
- href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
- text: "Start a free trial",
- },
- },
- {
- icon: GitPullRequest,
- title: "2. Connect GitHub",
- description: "Link your repositories so Roo Code can open PRs on your behalf.",
- },
- {
- icon: Settings,
- title: "3. Connect Linear",
- description: "Authorize via OAuth. No API keys to manage or rotate.",
- },
- {
- icon: CheckCircle,
- title: "4. Link & Start",
- description: "Map your Linear project to a repo, then assign or mention @Roo Code.",
- },
-]
-
-function LinearIcon({ className }: { className?: string }) {
- return (
-
- )
-}
-
-export default function LinearPage(): JSX.Element {
- return (
- <>
- {/* Hero Section */}
-
-
-
-
-
-
-
- Powered by Roo Code Cloud
-
-
- Turn Linear Issues into Pull Requests
-
-
- Assign development work to @Roo Code directly from Linear. Get PRs back without
- switching tools.
-
-
-
-
-
-
-
-
-
-
-
- {/* Value Props Section */}
-
-
-
-
-
- Why your team will love using Roo Code in Linear
-
-
- AI agents that understand context, keep your team in the loop, and deliver PRs you can
- review.
-
-
-
- {VALUE_PROPS.map((prop, index) => {
- const Icon = prop.icon
- return (
-
-
-
-
-
{prop.title}
-
{prop.description}
-
- )
- })}
-
-
-
-
- {/* Featured Workflow Section - temporarily commented out until video is ready
-
-
-
-
-
-
-
- Featured Workflow
-
-
Issue to Shipped Feature
-
- Stay in Linear from assignment to review. Roo Code keeps the issue updated and links the PR
- when it's ready.
-
-
-
-
-
- {/* YouTube Video Embed or Placeholder */}
- {/*
- {LINEAR_DEMO_YOUTUBE_ID ? (
-
- ) : (
-
-
-
- Demo Video Coming Soon
-
-
- See the workflow in action: assign an issue to @Roo Code and watch as it
- analyzes requirements, writes code, and opens a PR.
-
-
- )}
-
-
- {/* Workflow Steps */}
- {/*
- {WORKFLOW_STEPS.map((step) => (
-
-
-
- {step.step}
-
-
-
- {step.title}
-
-
- {step.description}
-
-
-
-
- ))}
-
-
-
-
-
- */}
-
- {/* Onboarding Section */}
-
-
-
-
Get started in minutes
-
- Connect Linear and start assigning issues to AI.
-
-
-
- {ONBOARDING_STEPS.map((step, index) => {
- const Icon = step.icon
- return (
-
-
-
-
-
{step.title}
-
- {step.description}
- {step.link && (
- <>
- {" "}
-
- {step.link.text} →
-
- >
- )}
-
-
- )
- })}
-
-
-
-
- {/* CTA Section */}
-
-
-
-
- Start using Roo Code in Linear
-
-
- Start a free 14 day Team trial.
-
-
-
-
-
- >
- )
-}
diff --git a/apps/web-roo-code/src/components/chromes/nav-bar.tsx b/apps/web-roo-code/src/components/chromes/nav-bar.tsx
index fda49dfc871..023114c2d3f 100644
--- a/apps/web-roo-code/src/components/chromes/nav-bar.tsx
+++ b/apps/web-roo-code/src/components/chromes/nav-bar.tsx
@@ -15,14 +15,6 @@ import { ScrollButton } from "@/components/ui"
import ThemeToggle from "@/components/chromes/theme-toggle"
import { Brain, ChevronDown, Cloud, Puzzle, Slack, X } from "lucide-react"
-function LinearIcon({ className }: { className?: string }) {
- return (
-
- )
-}
-
interface NavBarProps {
stars: string | null
downloads: string | null
@@ -68,12 +60,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
Roo Code for Slack
-
-
- Roo Code for Linear
-
@@ -216,12 +202,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
onClick={() => setIsMenuOpen(false)}>
Roo Code for Slack
- setIsMenuOpen(false)}>
- Roo Code for Linear
-
{
- const media = window.matchMedia("(prefers-reduced-motion: reduce)")
- const onChange = () => setReduced(media.matches)
- onChange()
-
- if (typeof media.addEventListener === "function") {
- media.addEventListener("change", onChange)
- return () => media.removeEventListener("change", onChange)
- }
-
- media.addListener?.(onChange)
- return () => media.removeListener?.(onChange)
- }, [])
-
- return reduced
-}
-
-type TypingDotsProps = {
- className?: string
-}
-
-function TypingDots({ className }: TypingDotsProps): JSX.Element {
- return (
-
-
-
-
-
- )
-}
-
-function LinearIcon({ className }: { className?: string }) {
- return (
-
- )
-}
-
-type ActivityRowProps = {
- item: ActivityItem
- isNew: boolean
- reduceMotion: boolean
-}
-
-function ActivityRow({ item, isNew, reduceMotion }: ActivityRowProps): JSX.Element {
- let animation = ""
- if (!reduceMotion && isNew) {
- animation = "animate-in fade-in slide-in-from-bottom-1 duration-300"
- }
-
- // Event items (status changes, etc.) - compact inline format
- if (item.kind === "event") {
- return (
-
-
- {item.avatarText}
-
-
{item.author}
-
{item.body}
-
·
-
{item.timeLabel}
-
- )
- }
-
- // PR link events
- if (item.kind === "pr-link") {
- return (
-
-
- {item.body}
- ·
- {item.timeLabel}
-
- )
- }
-
- // Comment items - more substantial with message body
- return (
-
-
- {item.avatarText}
-
-
-
- {item.author}
- ·
- {item.timeLabel}
-
-
{item.body}
-
-
- )
-}
-
-export type LinearIssueDemoProps = {
- className?: string
-}
-
-export function LinearIssueDemo({ className }: LinearIssueDemoProps): JSX.Element {
- const reduceMotion = usePrefersReducedMotion()
- const [stepIndex, setStepIndex] = useState(0)
- const scrollViewportRef = useRef(null)
-
- const activityItems: ActivityItem[] = useMemo(
- () => [
- {
- id: "a1",
- kind: "comment",
- author: "Jordan",
- avatarText: "J",
- avatarClassName: "bg-amber-600 text-white",
- body: (
-
- @Roo Code Can you implement this feature?
-
- ),
- timeLabel: "2m ago",
- },
- {
- id: "a2",
- kind: "comment",
- author: "Roo Code",
- avatarText: "R",
- avatarClassName: "bg-indigo-600 text-white",
- body: Analyzing issue requirements and codebase...,
- timeLabel: "2m ago",
- },
- {
- id: "a3",
- kind: "event",
- author: "Roo Code",
- avatarText: "R",
- avatarClassName: "bg-indigo-600 text-white",
- body: moved to In Progress,
- timeLabel: "2m ago",
- },
- {
- id: "a4",
- kind: "comment",
- author: "Roo Code",
- avatarText: "R",
- avatarClassName: "bg-indigo-600 text-white",
- body: Planning implementation: Settings component with light/dark toggle.,
- timeLabel: "1m ago",
- },
- {
- id: "a5",
- kind: "comment",
- author: "Jordan",
- avatarText: "J",
- avatarClassName: "bg-amber-600 text-white",
- body: (
-
- @Roo Code Please also add a "system" option
- that follows OS preference.
-
- ),
- timeLabel: "1m ago",
- },
- {
- id: "a6",
- kind: "comment",
- author: "Roo Code",
- avatarText: "R",
- avatarClassName: "bg-indigo-600 text-white",
- body: (
-
- Got it! Adding system preference detection using{" "}
-
- prefers-color-scheme
-
-
- ),
- timeLabel: "30s ago",
- },
- {
- id: "a7",
- kind: "pr-link",
- body: (
-
- Roo Code linked{" "}
- PR #847
-
- ),
- timeLabel: "just now",
- },
- {
- id: "a8",
- kind: "comment",
- author: "Roo Code",
- avatarText: "R",
- avatarClassName: "bg-indigo-600 text-white",
- body: (
-
-
- PR ready for review:{" "}
- #847
-
-
-
-
- feat: add theme toggle with system preference
-
-
+142 -12 · 3 files changed
-
-
- ),
- timeLabel: "just now",
- },
- ],
- [],
- )
-
- type DemoPhase =
- | { kind: "issue" }
- | { kind: "show"; activityIndex: number }
- | { kind: "typing"; activityIndex: number }
- | { kind: "reset" }
-
- const phases: DemoPhase[] = useMemo(() => {
- const next: DemoPhase[] = []
-
- next.push({ kind: "issue" })
-
- for (let activityIndex = 0; activityIndex < activityItems.length; activityIndex += 1) {
- const item = activityItems[activityIndex]
- if (item?.kind === "comment") {
- next.push({ kind: "typing", activityIndex })
- }
- next.push({ kind: "show", activityIndex })
- }
- next.push({ kind: "reset" })
- return next
- }, [activityItems])
-
- const lastShowPhaseIndex = useMemo(() => {
- let lastIndex = -1
- for (let idx = 0; idx < phases.length; idx += 1) {
- if (phases[idx]?.kind === "show") lastIndex = idx
- }
- return lastIndex
- }, [phases])
-
- useEffect(() => {
- if (reduceMotion) {
- setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
- return
- }
-
- const active = phases[stepIndex] ?? phases.at(0)
- const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
- const durationMs = (() => {
- const base = 2000
- if (active?.kind === "reset") return 500
- if (active?.kind === "issue") return 1500
- if (active?.kind === "typing") return 800
- return isLastMessageShow ? base * 2.5 : base
- })()
-
- const timer = window.setTimeout(() => {
- const nextIndex = (stepIndex + 1) % phases.length
- setStepIndex(nextIndex)
- }, durationMs)
-
- return () => window.clearTimeout(timer)
- }, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
-
- const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "issue" }
-
- function getVisibleCount(phase: DemoPhase): number {
- if (phase.kind === "reset" || phase.kind === "issue") return 0
- if (phase.kind === "typing") return phase.activityIndex
- return phase.activityIndex + 1
- }
-
- const visibleCount = getVisibleCount(activePhase)
- const visibleActivities = activityItems.slice(0, visibleCount)
- const typingTarget = activePhase.kind === "typing" ? activityItems[activePhase.activityIndex] : undefined
-
- useEffect(() => {
- const viewport = scrollViewportRef.current
- if (!viewport) return
-
- if (activePhase.kind === "reset" || activePhase.kind === "issue" || visibleCount <= 1) {
- viewport.scrollTo({ top: 0, behavior: "auto" })
- return
- }
-
- viewport.scrollTo({
- top: viewport.scrollHeight,
- behavior: reduceMotion ? "auto" : "smooth",
- })
- }, [activePhase.kind, reduceMotion, visibleCount])
-
- const issueVisible = activePhase.kind !== "reset"
-
- return (
-
-
- {/* Linear-style Header with breadcrumb */}
-
-
-
Frontend
-
-
FE-312
-
-
- Live demo
-
-
-
- {/* Issue Content */}
-
- {/* Issue Title */}
-
-
- Add dark mode toggle to settings
-
-
- Users should be able to switch between light and dark themes from the settings page. Persist
- preference to localStorage and apply immediately.
-
-
-
- {/* Activity Section */}
-
-
- Activity
- Unsubscribe
-
-
-
- {visibleActivities.map((item) => (
-
- ))}
-
- {typingTarget && typingTarget.kind === "comment" && (
-
-
- {typingTarget.avatarText}
-
-
-
-
- {typingTarget.author}
-
- typing
-
-
-
-
- )}
-
-
-
-
- {/* Comment Input */}
-
-
-
Leave a comment...
-
-
-
-
-
-
- {/* Progress indicator */}
-
-
- {activityItems.map((item, idx) => (
-
- ))}
-
-
-
-
- )
-}
diff --git a/packages/types/src/__tests__/skills.test.ts b/packages/types/src/__tests__/skills.test.ts
deleted file mode 100644
index c215f7cdbe9..00000000000
--- a/packages/types/src/__tests__/skills.test.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-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)
- })
-})
diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts
index d57ec616ff4..72c8b8256f8 100644
--- a/packages/types/src/global-settings.ts
+++ b/packages/types/src/global-settings.ts
@@ -199,7 +199,6 @@ export const globalSettingsSchema = z.object({
telemetrySetting: telemetrySettingsSchema.optional(),
mcpEnabled: z.boolean().optional(),
- enableMcpServerCreation: z.boolean().optional(),
mode: z.string().optional(),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index ad012b3761f..996ee781b28 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -19,7 +19,6 @@ 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"
diff --git a/packages/types/src/skills.ts b/packages/types/src/skills.ts
deleted file mode 100644
index 2c4ac176b0a..00000000000
--- a/packages/types/src/skills.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * 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 }
-}
diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts
index f90ef42ede4..03144055c9a 100644
--- a/packages/types/src/tool.ts
+++ b/packages/types/src/tool.ts
@@ -33,10 +33,10 @@ export const toolNames = [
"attempt_completion",
"switch_mode",
"new_task",
- "fetch_instructions",
"codebase_search",
"update_todo_list",
"run_slash_command",
+ "skill",
"generate_image",
"custom_tool",
] as const
diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts
index d8c421f4b8d..5670fa1ade9 100644
--- a/packages/types/src/vscode-extension-host.ts
+++ b/packages/types/src/vscode-extension-host.ts
@@ -18,7 +18,6 @@ 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"
@@ -109,7 +108,6 @@ export interface ExtensionMessage {
| "worktreeIncludeStatus"
| "branchWorktreeIncludeResult"
| "folderSelected"
- | "skills"
text?: string
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
checkpointWarning?: {
@@ -204,7 +202,6 @@ 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
@@ -362,7 +359,6 @@ export type ExtensionState = Pick<
experiments: Experiments // Map of experiment IDs to their enabled state
mcpEnabled: boolean
- enableMcpServerCreation: boolean
mode: string
customModes: ModeConfig[]
@@ -502,7 +498,6 @@ export interface WebviewMessage {
| "deleteMessageConfirm"
| "submitEditedMessage"
| "editMessageConfirm"
- | "enableMcpServerCreation"
| "remoteControlEnabled"
| "taskSyncEnabled"
| "searchCommits"
@@ -605,11 +600,6 @@ export interface WebviewMessage {
| "createWorktreeInclude"
| "checkoutBranch"
| "browseForWorktreePath"
- // Skills messages
- | "requestSkills"
- | "createSkill"
- | "deleteSkill"
- | "openSkillFile"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -644,9 +634,6 @@ 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
@@ -790,7 +777,6 @@ export interface ClineSayTool {
| "codebaseSearch"
| "readFile"
| "readCommandOutput"
- | "fetchInstructions"
| "listFilesTopLevel"
| "listFilesRecursive"
| "searchFiles"
@@ -801,6 +787,7 @@ export interface ClineSayTool {
| "imageGenerated"
| "runSlashCommand"
| "updateTodoList"
+ | "skill"
path?: string
// For readCommandOutput
readStart?: number
@@ -847,6 +834,8 @@ export interface ClineSayTool {
args?: string
source?: string
description?: string
+ // Properties for skill tool
+ skill?: string
}
// Must keep in sync with system prompt.
diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts
index 8aa369f74da..ecf649e2734 100644
--- a/src/core/assistant-message/NativeToolCallParser.ts
+++ b/src/core/assistant-message/NativeToolCallParser.ts
@@ -449,14 +449,6 @@ export class NativeToolCallParser {
}
break
- case "fetch_instructions":
- if (partialArgs.task !== undefined) {
- nativeArgs = {
- task: partialArgs.task,
- }
- }
- break
-
case "generate_image":
if (partialArgs.prompt !== undefined || partialArgs.path !== undefined) {
nativeArgs = {
@@ -476,6 +468,15 @@ export class NativeToolCallParser {
}
break
+ case "skill":
+ if (partialArgs.skill !== undefined) {
+ nativeArgs = {
+ skill: partialArgs.skill,
+ args: partialArgs.args,
+ }
+ }
+ break
+
case "search_files":
if (partialArgs.path !== undefined || partialArgs.regex !== undefined) {
nativeArgs = {
@@ -736,14 +737,6 @@ export class NativeToolCallParser {
}
break
- case "fetch_instructions":
- if (args.task !== undefined) {
- nativeArgs = {
- task: args.task,
- } as NativeArgsFor
- }
- break
-
case "generate_image":
if (args.prompt !== undefined && args.path !== undefined) {
nativeArgs = {
@@ -763,6 +756,15 @@ export class NativeToolCallParser {
}
break
+ case "skill":
+ if (args.skill !== undefined) {
+ nativeArgs = {
+ skill: args.skill,
+ args: args.args,
+ } as NativeArgsFor
+ }
+ break
+
case "search_files":
if (args.path !== undefined && args.regex !== undefined) {
nativeArgs = {
diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts
index db17bb97046..c0c7bf0eeb6 100644
--- a/src/core/assistant-message/presentAssistantMessage.ts
+++ b/src/core/assistant-message/presentAssistantMessage.ts
@@ -14,7 +14,6 @@ import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../sha
import { AskIgnoredError } from "../task/AskIgnoredError"
import { Task } from "../task/Task"
-import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
import { listFilesTool } from "../tools/ListFilesTool"
import { readFileTool } from "../tools/ReadFileTool"
import { readCommandOutputTool } from "../tools/ReadCommandOutputTool"
@@ -34,6 +33,7 @@ import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/Atte
import { newTaskTool } from "../tools/NewTaskTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
+import { skillTool } from "../tools/SkillTool"
import { generateImageTool } from "../tools/GenerateImageTool"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
@@ -370,8 +370,6 @@ export async function presentAssistantMessage(cline: Task) {
return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
}
return readFileTool.getReadFileToolDescription(block.name, block.params)
- case "fetch_instructions":
- return `[${block.name} for '${block.params.task}']`
case "write_to_file":
return `[${block.name} for '${block.params.path}']`
case "apply_diff":
@@ -417,6 +415,8 @@ export async function presentAssistantMessage(cline: Task) {
}
case "run_slash_command":
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
+ case "skill":
+ return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image":
return `[${block.name} for '${block.params.path}']`
default:
@@ -805,13 +805,6 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
- case "fetch_instructions":
- await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
- askApproval,
- handleError,
- pushToolResult,
- })
- break
case "list_files":
await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
askApproval,
@@ -915,6 +908,13 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
+ case "skill":
+ await skillTool.handle(cline, block as ToolUse<"skill">, {
+ askApproval,
+ handleError,
+ pushToolResult,
+ })
+ break
case "generate_image":
await checkpointSaveAndMark(cline)
await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
@@ -1094,7 +1094,6 @@ function containsXmlToolMarkup(text: string): boolean {
"codebase_search",
"edit_file",
"execute_command",
- "fetch_instructions",
"generate_image",
"list_files",
"new_task",
diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts
index f2951405010..f9de2ccfe36 100644
--- a/src/core/auto-approval/index.ts
+++ b/src/core/auto-approval/index.ts
@@ -151,14 +151,11 @@ export async function checkAutoApproval({
return { decision: "approve" }
}
- if (tool?.tool === "fetchInstructions") {
- if (tool.content === "create_mode") {
- return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
- }
-
- if (tool.content === "create_mcp_server") {
- return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
- }
+ // The skill tool only loads pre-defined instructions from built-in, global, or project skills.
+ // It does not read arbitrary files - skills must be explicitly installed/defined by the user.
+ // Auto-approval is intentional to provide a seamless experience when loading task instructions.
+ if (tool.tool === "skill") {
+ return { decision: "approve" }
}
if (tool?.tool === "switchMode") {
diff --git a/src/core/prompts/__tests__/add-custom-instructions.spec.ts b/src/core/prompts/__tests__/add-custom-instructions.spec.ts
index 79399f40b2b..b7813d0f5b8 100644
--- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts
+++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts
@@ -211,7 +211,6 @@ describe("addCustomInstructions", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -233,7 +232,6 @@ describe("addCustomInstructions", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -257,7 +255,6 @@ describe("addCustomInstructions", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -280,7 +277,6 @@ describe("addCustomInstructions", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
true, // partialReadsEnabled
diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
index 5399b92c651..0ec2956b317 100644
--- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts
+++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
@@ -105,7 +105,6 @@ describe("File-Based Custom System Prompt", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -142,7 +141,6 @@ describe("File-Based Custom System Prompt", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -187,7 +185,6 @@ describe("File-Based Custom System Prompt", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts
index d171e135077..91fb9350b4c 100644
--- a/src/core/prompts/__tests__/system-prompt.spec.ts
+++ b/src/core/prompts/__tests__/system-prompt.spec.ts
@@ -226,7 +226,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -248,7 +247,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -272,7 +270,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -294,7 +291,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -316,7 +312,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -324,6 +319,7 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-different-viewport-size.snap")
})
+
it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = vi.mocked(await import("vscode")) as any
@@ -364,7 +360,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -424,7 +419,6 @@ describe("SYSTEM_PROMPT", () => {
customModes, // customModes
"Global instructions", // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -461,7 +455,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -493,7 +486,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -523,7 +515,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -555,7 +546,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -587,7 +577,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -619,7 +608,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -654,6 +642,7 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("SYSTEM INFORMATION")
expect(prompt).toContain("OBJECTIVE")
})
+
afterAll(() => {
vi.restoreAllMocks()
})
diff --git a/src/core/prompts/instructions/create-mode.ts b/src/core/prompts/instructions/create-mode.ts
deleted file mode 100644
index 9623aae0cd1..00000000000
--- a/src/core/prompts/instructions/create-mode.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import * as path from "path"
-import * as vscode from "vscode"
-
-import { GlobalFileNames } from "../../../shared/globalFileNames"
-import { getSettingsDirectoryPath } from "../../../utils/storage"
-
-export async function createModeInstructions(context: vscode.ExtensionContext | undefined): Promise {
- if (!context) throw new Error("Missing VSCode Extension Context")
-
- const settingsDir = await getSettingsDirectoryPath(context.globalStorageUri.fsPath)
- const customModesPath = path.join(settingsDir, GlobalFileNames.customModes)
-
- return `
-Custom modes can be configured in two ways:
- 1. Globally via '${customModesPath}' (created automatically on startup)
- 2. Per-workspace via '.roomodes' in the workspace root directory
-
-When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes.
-
-If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file.
-
-- The following fields are required and must not be empty:
- * slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
- * name: The display name for the mode
- * roleDefinition: A detailed description of the mode's role and capabilities
- * groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
-
-- The following fields are optional but highly recommended:
- * description: A short, human-readable description of what this mode does (5 words)
- * whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions.
- * customInstructions: Additional instructions for how the mode should operate
-
-- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break."
-
-Both files should follow this structure (in YAML format):
-
-customModes:
- - slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
- name: Designer # Required: mode display name
- description: UI/UX design systems expert # Optional but recommended: short description (5 words)
- roleDefinition: >-
- You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
- - Creating and maintaining design systems
- - Implementing responsive and accessible web interfaces
- - Working with CSS, HTML, and modern frontend frameworks
- - Ensuring consistent user experiences across platforms # Required: non-empty
- whenToUse: >-
- Use this mode when creating or modifying UI components, implementing design systems,
- or ensuring responsive web interfaces. This mode is especially effective with CSS,
- HTML, and modern frontend frameworks. # Optional but recommended
- groups: # Required: array of tool groups (can be empty)
- - read # Read files group (read_file, fetch_instructions, search_files, list_files)
- - edit # Edit files group (apply_diff, write_to_file) - allows editing any file
- # Or with file restrictions:
- # - - edit
- # - fileRegex: \\.md$
- # description: Markdown files only # Edit group that only allows editing markdown files
- - browser # Browser group (browser_action)
- - command # Command group (execute_command)
- - mcp # MCP group (use_mcp_tool, access_mcp_resource)
- customInstructions: Additional instructions for the Designer mode # Optional`
-}
diff --git a/src/core/prompts/instructions/instructions.ts b/src/core/prompts/instructions/instructions.ts
deleted file mode 100644
index c1ff2a1899e..00000000000
--- a/src/core/prompts/instructions/instructions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { createMCPServerInstructions } from "./create-mcp-server"
-import { createModeInstructions } from "./create-mode"
-import { McpHub } from "../../../services/mcp/McpHub"
-import { DiffStrategy } from "../../../shared/tools"
-import * as vscode from "vscode"
-
-interface InstructionsDetail {
- mcpHub?: McpHub
- diffStrategy?: DiffStrategy
- context?: vscode.ExtensionContext
-}
-
-export async function fetchInstructions(text: string, detail: InstructionsDetail): Promise {
- switch (text) {
- case "create_mcp_server": {
- return await createMCPServerInstructions(detail.mcpHub, detail.diffStrategy)
- }
- case "create_mode": {
- return await createModeInstructions(detail.context)
- }
- default: {
- return ""
- }
- }
-}
diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts
index 1925405aa87..5c4ea2cf53a 100644
--- a/src/core/prompts/sections/modes.ts
+++ b/src/core/prompts/sections/modes.ts
@@ -5,17 +5,14 @@ import type { ModeConfig } from "@roo-code/types"
import { getAllModesWithPrompts } from "../../../shared/modes"
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
-export async function getModesSection(
- context: vscode.ExtensionContext,
- skipXmlExamples: boolean = false,
-): Promise {
+export async function getModesSection(context: vscode.ExtensionContext): Promise {
// Make sure path gets created
await ensureSettingsDirectoryExists(context)
// Get all modes with their overrides from extension state
const allModes = await getAllModesWithPrompts(context)
- let modesContent = `====
+ const modesContent = `====
MODES
@@ -34,18 +31,5 @@ ${allModes
})
.join("\n")}`
- if (!skipXmlExamples) {
- modesContent += `
-If the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool, like this:
-
-create_mode
-
-`
- } else {
- modesContent += `
-If the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool.
-`
- }
-
return modesContent
}
diff --git a/src/core/prompts/sections/skills.ts b/src/core/prompts/sections/skills.ts
index 53ba8b95f19..39cfca405b5 100644
--- a/src/core/prompts/sections/skills.ts
+++ b/src/core/prompts/sections/skills.ts
@@ -33,10 +33,11 @@ export async function getSkillsSection(
.map((skill) => {
const name = escapeXml(skill.name)
const description = escapeXml(skill.description)
- // Per the Agent Skills integration guidance for filesystem-based agents,
- // location should be an absolute path to the SKILL.md file.
- const location = escapeXml(skill.path)
- return ` \n ${name}\n ${description}\n ${location}\n `
+ // Only include location for file-based skills (not built-in)
+ // Built-in skills are loaded via the skill tool by name, not by path
+ const isFileBasedSkill = skill.source !== "built-in" && skill.path !== "built-in"
+ const locationLine = isFileBasedSkill ? `\n ${escapeXml(skill.path)}` : ""
+ return ` \n ${name}\n ${description}${locationLine}\n `
})
.join("\n")
@@ -62,9 +63,9 @@ Step 2: Branching Decision
- Select EXACTLY ONE skill.
- Prefer the most specific skill when multiple skills match.
-- Read the full SKILL.md file at the skill's .
-- Load the SKILL.md contents fully into context BEFORE continuing.
-- Follow the SKILL.md instructions precisely.
+- Use the skill tool to load the skill by name.
+- Load the skill's instructions fully into context BEFORE continuing.
+- Follow the skill instructions precisely.
- Do NOT respond outside the skill-defined flow.
@@ -74,15 +75,15 @@ Step 2: Branching Decision
CONSTRAINTS:
-- Do NOT load every SKILL.md up front.
-- Load SKILL.md ONLY after a skill is selected.
+- Do NOT load every skill up front.
+- Load skills ONLY after a skill is selected.
- Do NOT skip this check.
- FAILURE to perform this check is an error.
-- When a SKILL.md is loaded, ONLY the contents of SKILL.md are present.
-- Files linked from SKILL.md are NOT loaded automatically.
+- When a skill is loaded, ONLY the skill instructions are present.
+- Files linked from the skill are NOT loaded automatically.
- The model MUST explicitly decide to read a linked file based on task relevance.
- Do NOT assume the contents of linked files unless they have been explicitly read.
- Prefer reading the minimum necessary linked file.
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index b79f845d442..6404ec990e4 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -53,7 +53,6 @@ async function generatePrompt(
customModeConfigs?: ModeConfig[],
globalCustomInstructions?: string,
experiments?: Record,
- enableMcpServerCreation?: boolean,
language?: string,
rooIgnoreInstructions?: string,
partialReadsEnabled?: boolean,
@@ -127,7 +126,6 @@ export const SYSTEM_PROMPT = async (
customModes?: ModeConfig[],
globalCustomInstructions?: string,
experiments?: Record,
- enableMcpServerCreation?: boolean,
language?: string,
rooIgnoreInstructions?: string,
partialReadsEnabled?: boolean,
@@ -196,7 +194,6 @@ ${customInstructions}`
customModes,
globalCustomInstructions,
experiments,
- enableMcpServerCreation,
language,
rooIgnoreInstructions,
partialReadsEnabled,
diff --git a/src/core/prompts/tools/native-tools/fetch_instructions.ts b/src/core/prompts/tools/native-tools/fetch_instructions.ts
deleted file mode 100644
index 86ab184c58d..00000000000
--- a/src/core/prompts/tools/native-tools/fetch_instructions.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import type OpenAI from "openai"
-
-const FETCH_INSTRUCTIONS_DESCRIPTION = `Retrieve detailed instructions for performing a predefined task, such as creating an MCP server or creating a mode.`
-
-const TASK_PARAMETER_DESCRIPTION = `Task identifier to fetch instructions for`
-
-export default {
- type: "function",
- function: {
- name: "fetch_instructions",
- description: FETCH_INSTRUCTIONS_DESCRIPTION,
- strict: true,
- parameters: {
- type: "object",
- properties: {
- task: {
- type: "string",
- description: TASK_PARAMETER_DESCRIPTION,
- enum: ["create_mcp_server", "create_mode"],
- },
- },
- required: ["task"],
- additionalProperties: false,
- },
- },
-} satisfies OpenAI.Chat.ChatCompletionTool
diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts
index b6af18fa154..f23a7b2f28f 100644
--- a/src/core/prompts/tools/native-tools/index.ts
+++ b/src/core/prompts/tools/native-tools/index.ts
@@ -7,13 +7,13 @@ import attemptCompletion from "./attempt_completion"
import browserAction from "./browser_action"
import codebaseSearch from "./codebase_search"
import executeCommand from "./execute_command"
-import fetchInstructions from "./fetch_instructions"
import generateImage from "./generate_image"
import listFiles from "./list_files"
import newTask from "./new_task"
import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
+import skill from "./skill"
import searchAndReplace from "./search_and_replace"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
@@ -62,13 +62,13 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
browserAction,
codebaseSearch,
executeCommand,
- fetchInstructions,
generateImage,
listFiles,
newTask,
readCommandOutput,
createReadFileTool(readFileOptions),
runSlashCommand,
+ skill,
searchAndReplace,
searchReplace,
edit_file,
diff --git a/src/core/prompts/tools/native-tools/skill.ts b/src/core/prompts/tools/native-tools/skill.ts
new file mode 100644
index 00000000000..98a2d98cc8d
--- /dev/null
+++ b/src/core/prompts/tools/native-tools/skill.ts
@@ -0,0 +1,33 @@
+import type OpenAI from "openai"
+
+const SKILL_DESCRIPTION = `Load and execute a skill by name. Skills provide specialized instructions for common tasks like creating MCP servers or custom modes.
+
+Use this tool when you need to follow specific procedures documented in a skill. Available skills are listed in the AVAILABLE SKILLS section of the system prompt.`
+
+const SKILL_PARAMETER_DESCRIPTION = `Name of the skill to load (e.g., create-mcp-server, create-mode). Must match a skill name from the available skills list.`
+
+const ARGS_PARAMETER_DESCRIPTION = `Optional context or arguments to pass to the skill`
+
+export default {
+ type: "function",
+ function: {
+ name: "skill",
+ description: SKILL_DESCRIPTION,
+ strict: true,
+ parameters: {
+ type: "object",
+ properties: {
+ skill: {
+ type: "string",
+ description: SKILL_PARAMETER_DESCRIPTION,
+ },
+ args: {
+ type: ["string", "null"],
+ description: ARGS_PARAMETER_DESCRIPTION,
+ },
+ },
+ required: ["skill", "args"],
+ additionalProperties: false,
+ },
+ },
+} satisfies OpenAI.Chat.ChatCompletionTool
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index ff697d77a88..0c54e560b89 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -3758,7 +3758,6 @@ export class Task extends EventEmitter implements TaskLike {
customModePrompts,
customInstructions,
experiments,
- enableMcpServerCreation,
browserToolEnabled,
language,
maxConcurrentFileReads,
@@ -3797,7 +3796,6 @@ export class Task extends EventEmitter implements TaskLike {
customModes,
customInstructions,
experiments,
- enableMcpServerCreation,
language,
rooIgnoreInstructions,
maxReadFileLine !== -1,
diff --git a/src/core/tools/FetchInstructionsTool.ts b/src/core/tools/FetchInstructionsTool.ts
deleted file mode 100644
index f800e57fc4b..00000000000
--- a/src/core/tools/FetchInstructionsTool.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { type ClineSayTool } from "@roo-code/types"
-
-import { Task } from "../task/Task"
-import { fetchInstructions } from "../prompts/instructions/instructions"
-import { formatResponse } from "../prompts/responses"
-import type { ToolUse } from "../../shared/tools"
-
-import { BaseTool, ToolCallbacks } from "./BaseTool"
-
-interface FetchInstructionsParams {
- task: string
-}
-
-export class FetchInstructionsTool extends BaseTool<"fetch_instructions"> {
- readonly name = "fetch_instructions" as const
-
- async execute(params: FetchInstructionsParams, task: Task, callbacks: ToolCallbacks): Promise {
- const { handleError, pushToolResult, askApproval } = callbacks
- const { task: taskParam } = params
-
- try {
- if (!taskParam) {
- task.consecutiveMistakeCount++
- task.recordToolError("fetch_instructions")
- task.didToolFailInCurrentTurn = true
- pushToolResult(await task.sayAndCreateMissingParamError("fetch_instructions", "task"))
- return
- }
-
- task.consecutiveMistakeCount = 0
-
- const completeMessage = JSON.stringify({
- tool: "fetchInstructions",
- content: taskParam,
- } satisfies ClineSayTool)
-
- const didApprove = await askApproval("tool", completeMessage)
-
- if (!didApprove) {
- return
- }
-
- // Now fetch the content and provide it to the agent.
- const provider = task.providerRef.deref()
- const mcpHub = provider?.getMcpHub()
-
- if (!mcpHub) {
- throw new Error("MCP hub not available")
- }
-
- const diffStrategy = task.diffStrategy
- const context = provider?.context
- const content = await fetchInstructions(taskParam, { mcpHub, diffStrategy, context })
-
- if (!content) {
- pushToolResult(formatResponse.toolError(`Invalid instructions request: ${taskParam}`))
- return
- }
-
- pushToolResult(content)
- } catch (error) {
- await handleError("fetch instructions", error as Error)
- }
- }
-
- override async handlePartial(task: Task, block: ToolUse<"fetch_instructions">): Promise {
- const taskParam: string | undefined = block.params.task
- const sharedMessageProps: ClineSayTool = { tool: "fetchInstructions", content: taskParam }
-
- const partialMessage = JSON.stringify({ ...sharedMessageProps, content: undefined } satisfies ClineSayTool)
- await task.ask("tool", partialMessage, block.partial).catch(() => {})
- }
-}
-
-export const fetchInstructionsTool = new FetchInstructionsTool()
diff --git a/src/core/tools/SkillTool.ts b/src/core/tools/SkillTool.ts
new file mode 100644
index 00000000000..213cfd91ee8
--- /dev/null
+++ b/src/core/tools/SkillTool.ts
@@ -0,0 +1,112 @@
+import { Task } from "../task/Task"
+import { formatResponse } from "../prompts/responses"
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+import type { ToolUse } from "../../shared/tools"
+
+interface SkillParams {
+ skill: string
+ args?: string
+}
+
+export class SkillTool extends BaseTool<"skill"> {
+ readonly name = "skill" as const
+
+ async execute(params: SkillParams, task: Task, callbacks: ToolCallbacks): Promise {
+ const { skill: skillName, args } = params
+ const { askApproval, handleError, pushToolResult } = callbacks
+
+ try {
+ // Validate skill name parameter
+ if (!skillName) {
+ task.consecutiveMistakeCount++
+ task.recordToolError("skill")
+ task.didToolFailInCurrentTurn = true
+ pushToolResult(await task.sayAndCreateMissingParamError("skill", "skill"))
+ return
+ }
+
+ task.consecutiveMistakeCount = 0
+
+ // Get SkillsManager from provider
+ const provider = task.providerRef.deref()
+ const skillsManager = provider?.getSkillsManager()
+
+ if (!skillsManager) {
+ task.recordToolError("skill")
+ task.didToolFailInCurrentTurn = true
+ pushToolResult(formatResponse.toolError("Skills Manager not available"))
+ return
+ }
+
+ // Get current mode for skill resolution
+ const state = await provider?.getState()
+ const currentMode = state?.mode ?? "code"
+
+ // Fetch skill content
+ const skillContent = await skillsManager.getSkillContent(skillName, currentMode)
+
+ if (!skillContent) {
+ // Get available skills for error message
+ const availableSkills = skillsManager.getSkillsForMode(currentMode)
+ const skillNames = availableSkills.map((s) => s.name)
+
+ task.recordToolError("skill")
+ task.didToolFailInCurrentTurn = true
+ pushToolResult(
+ formatResponse.toolError(
+ `Skill '${skillName}' not found. Available skills: ${skillNames.join(", ") || "(none)"}`,
+ ),
+ )
+ return
+ }
+
+ // Build approval message
+ const toolMessage = JSON.stringify({
+ tool: "skill",
+ skill: skillName,
+ args: args,
+ source: skillContent.source,
+ description: skillContent.description,
+ })
+
+ const didApprove = await askApproval("tool", toolMessage)
+
+ if (!didApprove) {
+ return
+ }
+
+ // Build the result message
+ let result = `Skill: ${skillName}`
+
+ if (skillContent.description) {
+ result += `\nDescription: ${skillContent.description}`
+ }
+
+ if (args) {
+ result += `\nProvided arguments: ${args}`
+ }
+
+ result += `\nSource: ${skillContent.source}`
+ result += `\n\n--- Skill Instructions ---\n\n${skillContent.instructions}`
+
+ pushToolResult(result)
+ } catch (error) {
+ await handleError("executing skill", error as Error)
+ }
+ }
+
+ override async handlePartial(task: Task, block: ToolUse<"skill">): Promise {
+ const skillName: string | undefined = block.params.skill
+ const args: string | undefined = block.params.args
+
+ const partialMessage = JSON.stringify({
+ tool: "skill",
+ skill: skillName,
+ args: args,
+ })
+
+ await task.ask("tool", partialMessage, block.partial).catch(() => {})
+ }
+}
+
+export const skillTool = new SkillTool()
diff --git a/src/core/tools/__tests__/skillTool.spec.ts b/src/core/tools/__tests__/skillTool.spec.ts
new file mode 100644
index 00000000000..fc1b3396e50
--- /dev/null
+++ b/src/core/tools/__tests__/skillTool.spec.ts
@@ -0,0 +1,345 @@
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { skillTool } from "../SkillTool"
+import { Task } from "../../task/Task"
+import { formatResponse } from "../../prompts/responses"
+import type { ToolUse } from "../../../shared/tools"
+
+describe("skillTool", () => {
+ let mockTask: any
+ let mockCallbacks: any
+ let mockSkillsManager: any
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+
+ mockSkillsManager = {
+ getSkillContent: vi.fn(),
+ getSkillsForMode: vi.fn().mockReturnValue([]),
+ }
+
+ mockTask = {
+ consecutiveMistakeCount: 0,
+ recordToolError: vi.fn(),
+ didToolFailInCurrentTurn: false,
+ sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
+ ask: vi.fn().mockResolvedValue({}),
+ providerRef: {
+ deref: vi.fn().mockReturnValue({
+ getState: vi.fn().mockResolvedValue({ mode: "code" }),
+ getSkillsManager: vi.fn().mockReturnValue(mockSkillsManager),
+ }),
+ },
+ }
+
+ mockCallbacks = {
+ askApproval: vi.fn().mockResolvedValue(true),
+ handleError: vi.fn(),
+ pushToolResult: vi.fn(),
+ }
+ })
+
+ it("should handle missing skill parameter", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "",
+ },
+ }
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockTask.consecutiveMistakeCount).toBe(1)
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("skill")
+ expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("skill", "skill")
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith("Missing parameter error")
+ })
+
+ it("should handle skill not found", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "non-existent",
+ },
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(null)
+ mockSkillsManager.getSkillsForMode.mockReturnValue([{ name: "create-mcp-server" }])
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ formatResponse.toolError("Skill 'non-existent' not found. Available skills: create-mcp-server"),
+ )
+ })
+
+ it("should handle empty available skills list", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "non-existent",
+ },
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(null)
+ mockSkillsManager.getSkillsForMode.mockReturnValue([])
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ formatResponse.toolError("Skill 'non-existent' not found. Available skills: (none)"),
+ )
+ })
+
+ it("should successfully load built-in skill", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ },
+ }
+
+ const mockSkillContent = {
+ name: "create-mcp-server",
+ description: "Instructions for creating MCP servers",
+ source: "built-in",
+ instructions: "Step 1: Create the server...",
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.askApproval).toHaveBeenCalledWith(
+ "tool",
+ JSON.stringify({
+ tool: "skill",
+ skill: "create-mcp-server",
+ args: undefined,
+ source: "built-in",
+ description: "Instructions for creating MCP servers",
+ }),
+ )
+
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ `Skill: create-mcp-server
+Description: Instructions for creating MCP servers
+Source: built-in
+
+--- Skill Instructions ---
+
+Step 1: Create the server...`,
+ )
+ })
+
+ it("should successfully load skill with arguments", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ args: "weather API server",
+ },
+ }
+
+ const mockSkillContent = {
+ name: "create-mcp-server",
+ description: "Instructions for creating MCP servers",
+ source: "built-in",
+ instructions: "Step 1: Create the server...",
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ `Skill: create-mcp-server
+Description: Instructions for creating MCP servers
+Provided arguments: weather API server
+Source: built-in
+
+--- Skill Instructions ---
+
+Step 1: Create the server...`,
+ )
+ })
+
+ it("should handle user rejection", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ },
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue({
+ name: "create-mcp-server",
+ description: "Test",
+ source: "built-in",
+ instructions: "Test instructions",
+ })
+
+ mockCallbacks.askApproval.mockResolvedValue(false)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled()
+ })
+
+ it("should handle partial block", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {
+ skill: "create-mcp-server",
+ args: "",
+ },
+ partial: true,
+ }
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockTask.ask).toHaveBeenCalledWith(
+ "tool",
+ JSON.stringify({
+ tool: "skill",
+ skill: "create-mcp-server",
+ args: "",
+ }),
+ true,
+ )
+
+ expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled()
+ })
+
+ it("should handle errors during execution", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ },
+ }
+
+ const error = new Error("Test error")
+ mockSkillsManager.getSkillContent.mockRejectedValue(error)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.handleError).toHaveBeenCalledWith("executing skill", error)
+ })
+
+ it("should reset consecutive mistake count on valid skill", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ },
+ }
+
+ mockTask.consecutiveMistakeCount = 5
+
+ const mockSkillContent = {
+ name: "create-mcp-server",
+ description: "Test",
+ source: "built-in",
+ instructions: "Test instructions",
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockTask.consecutiveMistakeCount).toBe(0)
+ })
+
+ it("should handle Skills Manager not available", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "create-mcp-server",
+ },
+ }
+
+ mockTask.providerRef.deref = vi.fn().mockReturnValue({
+ getState: vi.fn().mockResolvedValue({ mode: "code" }),
+ getSkillsManager: vi.fn().mockReturnValue(undefined),
+ })
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockTask.recordToolError).toHaveBeenCalledWith("skill")
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ formatResponse.toolError("Skills Manager not available"),
+ )
+ })
+
+ it("should load project skill", async () => {
+ const block: ToolUse<"skill"> = {
+ type: "tool_use" as const,
+ name: "skill" as const,
+ params: {},
+ partial: false,
+ nativeArgs: {
+ skill: "my-project-skill",
+ },
+ }
+
+ const mockSkillContent = {
+ name: "my-project-skill",
+ description: "A custom project skill",
+ source: "project",
+ instructions: "Follow these project-specific instructions...",
+ }
+
+ mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent)
+
+ await skillTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.askApproval).toHaveBeenCalledWith(
+ "tool",
+ JSON.stringify({
+ tool: "skill",
+ skill: "my-project-skill",
+ args: undefined,
+ source: "project",
+ description: "A custom project skill",
+ }),
+ )
+
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ `Skill: my-project-skill
+Description: A custom project skill
+Source: project
+
+--- Skill Instructions ---
+
+Follow these project-specific instructions...`,
+ )
+ })
+})
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index b101bee7d29..e722ce37f85 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2024,7 +2024,6 @@ export class ClineProvider
terminalZshP10k,
terminalZdotdir,
mcpEnabled,
- enableMcpServerCreation,
currentApiConfigName,
listApiConfigMeta,
pinnedApiConfigs,
@@ -2162,7 +2161,6 @@ export class ClineProvider
terminalZshP10k: terminalZshP10k ?? false,
terminalZdotdir: terminalZdotdir ?? false,
mcpEnabled: mcpEnabled ?? true,
- enableMcpServerCreation: enableMcpServerCreation ?? true,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
@@ -2408,7 +2406,6 @@ export class ClineProvider
mode: stateValues.mode ?? defaultModeSlug,
language: stateValues.language ?? formatLanguage(vscode.env.language),
mcpEnabled: stateValues.mcpEnabled ?? true,
- enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
mcpServers: this.mcpHub?.getAllServers() ?? [],
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts
index cacaf26004d..c08ff8cad92 100644
--- a/src/core/webview/__tests__/ClineProvider.spec.ts
+++ b/src/core/webview/__tests__/ClineProvider.spec.ts
@@ -315,6 +315,7 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({
vi.mock("../diff/strategies/multi-search-replace", () => ({
MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({
+ getToolDescription: () => "test",
getName: () => "test-strategy",
applyDiff: vi.fn(),
})),
@@ -557,7 +558,6 @@ describe("ClineProvider", () => {
writeDelayMs: 1000,
browserViewportSize: "900x600",
mcpEnabled: true,
- enableMcpServerCreation: false,
mode: defaultModeSlug,
customModes: [],
experiments: experimentDefault,
@@ -1349,7 +1349,6 @@ describe("ClineProvider", () => {
apiProvider: "openrouter" as const,
},
mcpEnabled: true,
- enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -1374,7 +1373,6 @@ describe("ClineProvider", () => {
apiProvider: "openrouter" as const,
},
mcpEnabled: false,
- enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -1431,38 +1429,6 @@ describe("ClineProvider", () => {
)
})
- test("generates system prompt with various configurations", async () => {
- await provider.resolveWebviewView(mockWebviewView)
-
- // Mock getState with typical configuration
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- apiProvider: "openrouter",
- apiModelId: "test-model",
- },
- customModePrompts: {},
- mode: "code",
- enableMcpServerCreation: true,
- mcpEnabled: false,
- browserViewportSize: "900x600",
- experiments: experimentDefault,
- browserToolEnabled: true,
- } as any)
-
- // Trigger getSystemPrompt
- const handler = getMessageHandler()
- await handler({ type: "getSystemPrompt", mode: "code" })
-
- // Verify system prompt was generated and sent
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- })
-
test("uses correct mode-specific instructions when mode is specified", async () => {
await provider.resolveWebviewView(mockWebviewView)
@@ -1475,7 +1441,6 @@ describe("ClineProvider", () => {
architect: { customInstructions: "Architect mode instructions" },
},
mode: "architect",
- enableMcpServerCreation: false,
mcpEnabled: false,
browserViewportSize: "900x600",
experiments: experimentDefault,
diff --git a/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts b/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts
index 702c932fd5d..3b521c0f14b 100644
--- a/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts
+++ b/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts
@@ -60,7 +60,6 @@ function makeProviderStub() {
browserViewportSize: "900x600",
mcpEnabled: false,
experiments: {},
- enableMcpServerCreation: false,
browserToolEnabled: true, // critical: enabled in settings
language: "en",
maxReadFileLine: -1,
diff --git a/src/core/webview/__tests__/skillsMessageHandler.spec.ts b/src/core/webview/__tests__/skillsMessageHandler.spec.ts
deleted file mode 100644
index 900796dd6c3..00000000000
--- a/src/core/webview/__tests__/skillsMessageHandler.spec.ts
+++ /dev/null
@@ -1,334 +0,0 @@
-// npx vitest run src/core/webview/__tests__/skillsMessageHandler.spec.ts
-
-import type { SkillMetadata, WebviewMessage } from "@roo-code/types"
-import type { ClineProvider } from "../ClineProvider"
-
-// Mock vscode first
-vi.mock("vscode", () => {
- const showErrorMessage = vi.fn()
-
- return {
- window: {
- showErrorMessage,
- },
- }
-})
-
-// Mock open-file
-vi.mock("../../../integrations/misc/open-file", () => ({
- openFile: vi.fn(),
-}))
-
-// Mock i18n
-vi.mock("../../../i18n", () => ({
- t: (key: string, params?: Record) => {
- const translations: Record = {
- "skills:errors.missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
- "skills:errors.manager_unavailable": "Skills manager not available",
- "skills:errors.missing_delete_fields": "Missing required fields: skillName or source",
- "skills:errors.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',
- )
- })
- })
-})
diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts
index ed1ab9e2726..b6f77d3842c 100644
--- a/src/core/webview/generateSystemPrompt.ts
+++ b/src/core/webview/generateSystemPrompt.ts
@@ -17,7 +17,6 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
browserViewportSize,
mcpEnabled,
experiments,
- enableMcpServerCreation,
browserToolEnabled,
language,
maxReadFileLine,
@@ -69,7 +68,6 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
customModes,
customInstructions,
experiments,
- enableMcpServerCreation,
language,
rooIgnoreInstructions,
maxReadFileLine !== -1,
diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts
deleted file mode 100644
index 649c036b4cc..00000000000
--- a/src/core/webview/skillsMessageHandler.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import * as vscode from "vscode"
-
-import type { SkillMetadata, WebviewMessage } from "@roo-code/types"
-
-import type { ClineProvider } from "./ClineProvider"
-import { openFile } from "../../integrations/misc/open-file"
-import { t } from "../../i18n"
-
-/**
- * Handles the requestSkills message - returns all skills metadata
- */
-export async function handleRequestSkills(provider: ClineProvider): Promise {
- try {
- const skillsManager = provider.getSkillsManager()
- if (skillsManager) {
- const skills = skillsManager.getSkillsMetadata()
- await provider.postMessageToWebview({ type: "skills", skills })
- return skills
- } else {
- await provider.postMessageToWebview({ type: "skills", skills: [] })
- return []
- }
- } catch (error) {
- provider.log(`Error fetching skills: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
- await provider.postMessageToWebview({ type: "skills", skills: [] })
- return []
- }
-}
-
-/**
- * Handles the createSkill message - creates a new skill
- */
-export async function handleCreateSkill(
- provider: ClineProvider,
- message: WebviewMessage,
-): Promise {
- try {
- const skillName = message.skillName
- const source = message.source
- const skillDescription = message.skillDescription
- const skillMode = message.skillMode
-
- if (!skillName || !source || !skillDescription) {
- throw new Error(t("skills:errors.missing_create_fields"))
- }
-
- const skillsManager = provider.getSkillsManager()
- if (!skillsManager) {
- throw new Error(t("skills:errors.manager_unavailable"))
- }
-
- const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, skillMode)
-
- // Open the created file in the editor
- openFile(createdPath)
-
- // Send updated skills list
- const skills = skillsManager.getSkillsMetadata()
- await provider.postMessageToWebview({ type: "skills", skills })
- return skills
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Error creating skill: ${errorMessage}`)
- vscode.window.showErrorMessage(`Failed to create skill: ${errorMessage}`)
- return undefined
- }
-}
-
-/**
- * Handles the deleteSkill message - deletes a skill
- */
-export async function handleDeleteSkill(
- provider: ClineProvider,
- message: WebviewMessage,
-): Promise {
- try {
- const skillName = message.skillName
- const source = message.source
- const skillMode = message.skillMode
-
- if (!skillName || !source) {
- throw new Error(t("skills:errors.missing_delete_fields"))
- }
-
- 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 {
- 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}`)
- }
-}
diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts
index 8246dda472e..cc4b1a27e9e 100644
--- a/src/core/webview/webviewMessageHandler.ts
+++ b/src/core/webview/webviewMessageHandler.ts
@@ -32,7 +32,6 @@ 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"
@@ -1444,10 +1443,6 @@ export const webviewMessageHandler = async (
}
break
}
- case "enableMcpServerCreation":
- await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
- await provider.postStateToWebview()
- break
case "remoteControlEnabled":
try {
await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false })
@@ -2975,22 +2970,6 @@ 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) {
diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json
deleted file mode 100644
index cce8bf45b0e..00000000000
--- a/src/i18n/locales/ca/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\""
- }
-}
diff --git a/src/i18n/locales/de/skills.json b/src/i18n/locales/de/skills.json
deleted file mode 100644
index 90356110408..00000000000
--- a/src/i18n/locales/de/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/en/skills.json b/src/i18n/locales/en/skills.json
deleted file mode 100644
index 9cf7369bc9b..00000000000
--- a/src/i18n/locales/en/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json
deleted file mode 100644
index d6d0727262f..00000000000
--- a/src/i18n/locales/es/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\""
- }
-}
diff --git a/src/i18n/locales/fr/skills.json b/src/i18n/locales/fr/skills.json
deleted file mode 100644
index 1337cb49b8e..00000000000
--- a/src/i18n/locales/fr/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/hi/skills.json b/src/i18n/locales/hi/skills.json
deleted file mode 100644
index bd5235c24db..00000000000
--- a/src/i18n/locales/hi/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\" नहीं मिला"
- }
-}
diff --git a/src/i18n/locales/id/skills.json b/src/i18n/locales/id/skills.json
deleted file mode 100644
index 0d9958a7744..00000000000
--- a/src/i18n/locales/id/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/it/skills.json b/src/i18n/locales/it/skills.json
deleted file mode 100644
index fa0fe6559e8..00000000000
--- a/src/i18n/locales/it/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/ja/skills.json b/src/i18n/locales/ja/skills.json
deleted file mode 100644
index baef99a5012..00000000000
--- a/src/i18n/locales/ja/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}」が見つかりません"
- }
-}
diff --git a/src/i18n/locales/ko/skills.json b/src/i18n/locales/ko/skills.json
deleted file mode 100644
index 0561b10dbbb..00000000000
--- a/src/i18n/locales/ko/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\"을(를) 찾을 수 없습니다"
- }
-}
diff --git a/src/i18n/locales/nl/skills.json b/src/i18n/locales/nl/skills.json
deleted file mode 100644
index 2a6fd2f733b..00000000000
--- a/src/i18n/locales/nl/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/pl/skills.json b/src/i18n/locales/pl/skills.json
deleted file mode 100644
index 6f0fb3a7d48..00000000000
--- a/src/i18n/locales/pl/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\""
- }
-}
diff --git a/src/i18n/locales/pt-BR/skills.json b/src/i18n/locales/pt-BR/skills.json
deleted file mode 100644
index b54655f483c..00000000000
--- a/src/i18n/locales/pt-BR/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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"
- }
-}
diff --git a/src/i18n/locales/ru/skills.json b/src/i18n/locales/ru/skills.json
deleted file mode 100644
index 7feee5d6c29..00000000000
--- a/src/i18n/locales/ru/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\" не найден"
- }
-}
diff --git a/src/i18n/locales/tr/skills.json b/src/i18n/locales/tr/skills.json
deleted file mode 100644
index 2e01d37378a..00000000000
--- a/src/i18n/locales/tr/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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ı"
- }
-}
diff --git a/src/i18n/locales/vi/skills.json b/src/i18n/locales/vi/skills.json
deleted file mode 100644
index bc3074a1c84..00000000000
--- a/src/i18n/locales/vi/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\""
- }
-}
diff --git a/src/i18n/locales/zh-CN/skills.json b/src/i18n/locales/zh-CN/skills.json
deleted file mode 100644
index 629aaeb6d74..00000000000
--- a/src/i18n/locales/zh-CN/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}\""
- }
-}
diff --git a/src/i18n/locales/zh-TW/skills.json b/src/i18n/locales/zh-TW/skills.json
deleted file mode 100644
index 10705f4cce8..00000000000
--- a/src/i18n/locales/zh-TW/skills.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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}}」"
- }
-}
diff --git a/src/package.json b/src/package.json
index bf4a009a946..53c7554d5eb 100644
--- a/src/package.json
+++ b/src/package.json
@@ -439,6 +439,8 @@
"pretest": "turbo run bundle --cwd ..",
"test": "vitest run",
"format": "prettier --write .",
+ "generate:skills": "tsx services/skills/generate-built-in-skills.ts",
+ "prebundle": "pnpm generate:skills",
"bundle": "node esbuild.mjs",
"vscode:prepublish": "pnpm bundle --production",
"vsix": "mkdirp ../bin && vsce package --no-dependencies --out ../bin",
diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts
index f9d27255f81..1c61b5b1761 100644
--- a/src/services/skills/SkillsManager.ts
+++ b/src/services/skills/SkillsManager.ts
@@ -1,6 +1,5 @@
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"
@@ -9,12 +8,7 @@ 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"
+import { getBuiltInSkills, getBuiltInSkillContent } from "./built-in-skills"
// Re-export for convenience
export type { SkillMetadata, SkillContent }
@@ -123,11 +117,23 @@ export class SkillsManager {
return
}
- // 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}`)
+ // 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)`,
+ )
return
}
@@ -159,13 +165,19 @@ export class SkillsManager {
/**
* Get skills available for the current mode.
- * Resolves overrides: project > global, mode-specific > generic.
+ * Resolves overrides: project > global > built-in, mode-specific > generic.
*
* @param currentMode - The current mode slug (e.g., 'code', 'architect')
*/
getSkillsForMode(currentMode: string): SkillMetadata[] {
const resolvedSkills = new Map()
+ // First, add built-in skills (lowest priority)
+ for (const skill of getBuiltInSkills()) {
+ resolvedSkills.set(skill.name, skill)
+ }
+
+ // Then, add discovered skills (will override built-in skills with same name)
for (const skill of this.skills.values()) {
// Skip mode-specific skills that don't match current mode
if (skill.mode && skill.mode !== currentMode) continue
@@ -189,12 +201,22 @@ export class SkillsManager {
/**
* Determine if newSkill should override existingSkill based on priority rules.
- * Priority: project > global, mode-specific > generic
+ * Priority: project > global > built-in, mode-specific > generic
*/
private shouldOverrideSkill(existing: SkillMetadata, newSkill: SkillMetadata): boolean {
- // Project always overrides global
- if (newSkill.source === "project" && existing.source === "global") return true
- if (newSkill.source === "global" && existing.source === "project") return false
+ // Define source priority: project > global > built-in
+ const sourcePriority: Record = {
+ project: 3,
+ global: 2,
+ "built-in": 1,
+ }
+
+ const existingPriority = sourcePriority[existing.source] ?? 0
+ const newPriority = sourcePriority[newSkill.source] ?? 0
+
+ // Higher priority source always wins
+ if (newPriority > existingPriority) return true
+ if (newPriority < existingPriority) return false
// Same source: mode-specific overrides generic
if (newSkill.mode && !existing.mode) return true
@@ -219,12 +241,21 @@ export class SkillsManager {
const modeSkills = this.getSkillsForMode(currentMode)
skill = modeSkills.find((s) => s.name === name)
} else {
- // Fall back to any skill with this name
+ // Fall back to any skill with this name (check discovered skills first, then built-in)
skill = Array.from(this.skills.values()).find((s) => s.name === name)
+ if (!skill) {
+ skill = getBuiltInSkills().find((s) => s.name === name)
+ }
}
if (!skill) return null
+ // For built-in skills, use the built-in content
+ if (skill.source === "built-in") {
+ return getBuiltInSkillContent(name)
+ }
+
+ // For file-based skills, read from disk
const fileContent = await fs.readFile(skill.path, "utf-8")
const { content: body } = matter(fileContent)
@@ -234,146 +265,6 @@ export class SkillsManager {
}
}
- /**
- * Get all skills metadata (for UI display)
- * Returns skills from all sources without content
- */
- getSkillsMetadata(): SkillMetadata[] {
- return this.getAllSkills()
- }
-
- /**
- * Get a skill by name, source, and optionally mode
- */
- getSkill(name: string, source: "global" | "project", mode?: string): SkillMetadata | undefined {
- const skillKey = this.getSkillKey(name, source, mode)
- return this.skills.get(skillKey)
- }
-
- /**
- * Validate skill name per agentskills.io spec using shared validation.
- * Converts error codes to user-friendly error messages.
- */
- private validateSkillName(name: string): { valid: boolean; error?: string } {
- const result = validateSkillNameShared(name)
- if (!result.valid) {
- return { valid: false, error: this.getSkillNameErrorMessage(name, result.error!) }
- }
- return { valid: true }
- }
-
- /**
- * Convert skill name validation error code to a user-friendly error message.
- */
- private getSkillNameErrorMessage(name: string, error: SkillNameValidationError): string {
- switch (error) {
- case SkillNameValidationError.Empty:
- return t("skills:errors.name_length", { maxLength: SKILL_NAME_MAX_LENGTH, length: name.length })
- case SkillNameValidationError.TooLong:
- return t("skills:errors.name_length", { maxLength: SKILL_NAME_MAX_LENGTH, length: name.length })
- case SkillNameValidationError.InvalidFormat:
- return t("skills:errors.name_format")
- }
- }
-
- /**
- * Create a new skill
- * @param name - Skill name (must be valid per agentskills.io spec)
- * @param source - "global" or "project"
- * @param description - Skill description
- * @param mode - Optional mode restriction (creates in skills-{mode}/ directory)
- * @returns Path to created SKILL.md file
- */
- async createSkill(name: string, source: "global" | "project", description: string, mode?: string): Promise {
- // Validate skill name
- const validation = this.validateSkillName(name)
- if (!validation.valid) {
- throw new Error(validation.error)
- }
-
- // Validate description
- const trimmedDescription = description.trim()
- if (trimmedDescription.length < 1 || trimmedDescription.length > 1024) {
- throw new Error(t("skills:errors.description_length", { length: trimmedDescription.length }))
- }
-
- // Determine base directory
- let baseDir: string
- if (source === "global") {
- baseDir = getGlobalRooDirectory()
- } else {
- const provider = this.providerRef.deref()
- if (!provider?.cwd) {
- throw new Error(t("skills:errors.no_workspace"))
- }
- baseDir = path.join(provider.cwd, ".roo")
- }
-
- // Determine skills directory (with optional mode suffix)
- const skillsDirName = mode ? `skills-${mode}` : "skills"
- const skillsDir = path.join(baseDir, skillsDirName)
- const skillDir = path.join(skillsDir, name)
- const skillMdPath = path.join(skillDir, "SKILL.md")
-
- // Check if skill already exists
- if (await fileExists(skillMdPath)) {
- throw new Error(t("skills:errors.already_exists", { name, path: skillMdPath }))
- }
-
- // Create the skill directory
- await fs.mkdir(skillDir, { recursive: true })
-
- // Generate SKILL.md content with frontmatter
- const titleName = name
- .split("-")
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(" ")
-
- const skillContent = `---
-name: ${name}
-description: ${trimmedDescription}
----
-
-# ${titleName}
-
-## Instructions
-
-Add your skill instructions here.
-`
-
- // Write the SKILL.md file
- await fs.writeFile(skillMdPath, skillContent, "utf-8")
-
- // Refresh skills list
- await this.discoverSkills()
-
- return skillMdPath
- }
-
- /**
- * Delete a skill
- * @param name - Skill name to delete
- * @param source - Where the skill is located
- * @param mode - Optional mode (to locate in skills-{mode}/ directory)
- */
- async deleteSkill(name: string, source: "global" | "project", mode?: string): Promise {
- // Find the skill
- const skill = this.getSkill(name, source, mode)
- if (!skill) {
- const modeInfo = mode ? ` (mode: ${mode})` : ""
- throw new Error(t("skills:errors.not_found", { name, source, modeInfo }))
- }
-
- // Get the skill directory (parent of SKILL.md)
- const skillDir = path.dirname(skill.path)
-
- // Delete the entire skill directory
- await fs.rm(skillDir, { recursive: true, force: true })
-
- // Refresh skills list
- await this.discoverSkills()
- }
-
/**
* Get all skills directories to scan, including mode-specific directories.
*/
diff --git a/src/services/skills/__tests__/SkillsManager.spec.ts b/src/services/skills/__tests__/SkillsManager.spec.ts
index 858c74fb5d7..aaf2792626b 100644
--- a/src/services/skills/__tests__/SkillsManager.spec.ts
+++ b/src/services/skills/__tests__/SkillsManager.spec.ts
@@ -1,29 +1,16 @@
import * as path from "path"
// Use vi.hoisted to ensure mocks are available during hoisting
-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(),
-}))
+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(),
+ }))
// Platform-agnostic test paths
// Use forward slashes for consistency, then normalize with path.normalize
@@ -41,17 +28,11 @@ 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
@@ -82,20 +63,12 @@ vi.mock("../../roo-config", () => ({
fileExists: mockFileExists,
}))
-// Mock i18n
-vi.mock("../../../i18n", () => ({
- t: (key: string, params?: Record) => {
- const translations: Record = {
- "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
- },
+// Mock built-in skills to isolate tests from actual built-in skills
+vi.mock("../built-in-skills", () => ({
+ getBuiltInSkills: () => [],
+ getBuiltInSkillContent: () => null,
+ isBuiltInSkill: () => false,
+ getBuiltInSkillNames: () => [],
}))
import { SkillsManager } from "../SkillsManager"
@@ -862,268 +835,4 @@ 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")
- })
- })
})
diff --git a/src/services/skills/__tests__/generate-built-in-skills.spec.ts b/src/services/skills/__tests__/generate-built-in-skills.spec.ts
new file mode 100644
index 00000000000..10b44b87163
--- /dev/null
+++ b/src/services/skills/__tests__/generate-built-in-skills.spec.ts
@@ -0,0 +1,175 @@
+/**
+ * Tests for the built-in skills generation script validation logic.
+ *
+ * Note: These tests focus on the validation functions since the main script
+ * is designed to be run as a CLI tool. The actual generation is tested
+ * via the integration with the build process.
+ */
+
+describe("generate-built-in-skills validation", () => {
+ describe("validateSkillName", () => {
+ // Validation function extracted from the generation script
+ function validateSkillName(name: string): string[] {
+ const errors: string[] = []
+
+ if (name.length < 1 || name.length > 64) {
+ errors.push(`Name must be 1-64 characters (got ${name.length})`)
+ }
+
+ const nameFormat = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
+ if (!nameFormat.test(name)) {
+ errors.push(
+ "Name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)",
+ )
+ }
+
+ return errors
+ }
+
+ it("should accept valid skill names", () => {
+ expect(validateSkillName("mcp-builder")).toHaveLength(0)
+ expect(validateSkillName("create-mode")).toHaveLength(0)
+ expect(validateSkillName("pdf-processing")).toHaveLength(0)
+ expect(validateSkillName("a")).toHaveLength(0)
+ expect(validateSkillName("skill123")).toHaveLength(0)
+ expect(validateSkillName("my-skill-v2")).toHaveLength(0)
+ })
+
+ it("should reject names with uppercase letters", () => {
+ const errors = validateSkillName("Create-MCP-Server")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("lowercase")
+ })
+
+ it("should reject names with leading hyphen", () => {
+ const errors = validateSkillName("-my-skill")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("leading/trailing hyphen")
+ })
+
+ it("should reject names with trailing hyphen", () => {
+ const errors = validateSkillName("my-skill-")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("leading/trailing hyphen")
+ })
+
+ it("should reject names with consecutive hyphens", () => {
+ const errors = validateSkillName("my--skill")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("consecutive hyphens")
+ })
+
+ it("should reject empty names", () => {
+ const errors = validateSkillName("")
+ expect(errors.length).toBeGreaterThan(0)
+ })
+
+ it("should reject names longer than 64 characters", () => {
+ const longName = "a".repeat(65)
+ const errors = validateSkillName(longName)
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("1-64 characters")
+ })
+
+ it("should reject names with special characters", () => {
+ expect(validateSkillName("my_skill").length).toBeGreaterThan(0)
+ expect(validateSkillName("my.skill").length).toBeGreaterThan(0)
+ expect(validateSkillName("my skill").length).toBeGreaterThan(0)
+ })
+ })
+
+ describe("validateDescription", () => {
+ // Validation function extracted from the generation script
+ function validateDescription(description: string): string[] {
+ const errors: string[] = []
+ const trimmed = description.trim()
+
+ if (trimmed.length < 1 || trimmed.length > 1024) {
+ errors.push(`Description must be 1-1024 characters (got ${trimmed.length})`)
+ }
+
+ return errors
+ }
+
+ it("should accept valid descriptions", () => {
+ expect(validateDescription("A short description")).toHaveLength(0)
+ expect(validateDescription("x")).toHaveLength(0)
+ expect(validateDescription("x".repeat(1024))).toHaveLength(0)
+ })
+
+ it("should reject empty descriptions", () => {
+ const errors = validateDescription("")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("1-1024 characters")
+ })
+
+ it("should reject whitespace-only descriptions", () => {
+ const errors = validateDescription(" ")
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("got 0")
+ })
+
+ it("should reject descriptions longer than 1024 characters", () => {
+ const longDesc = "x".repeat(1025)
+ const errors = validateDescription(longDesc)
+ expect(errors).toHaveLength(1)
+ expect(errors[0]).toContain("got 1025")
+ })
+ })
+
+ describe("escapeForTemplateLiteral", () => {
+ // Escape function extracted from the generation script
+ function escapeForTemplateLiteral(str: string): string {
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
+ }
+
+ it("should escape backticks", () => {
+ expect(escapeForTemplateLiteral("code `example`")).toBe("code \\`example\\`")
+ })
+
+ it("should escape template literal interpolation", () => {
+ expect(escapeForTemplateLiteral("value: ${foo}")).toBe("value: \\${foo}")
+ })
+
+ it("should escape backslashes", () => {
+ expect(escapeForTemplateLiteral("path\\to\\file")).toBe("path\\\\to\\\\file")
+ })
+
+ it("should handle combined escapes", () => {
+ const input = "const x = `${value}`"
+ const expected = "const x = \\`\\${value}\\`"
+ expect(escapeForTemplateLiteral(input)).toBe(expected)
+ })
+ })
+})
+
+describe("built-in skills integration", () => {
+ it("should have valid skill names matching directory names", async () => {
+ // Import the generated built-in skills
+ const { getBuiltInSkills, getBuiltInSkillContent } = await import("../built-in-skills")
+
+ const skills = getBuiltInSkills()
+
+ // Verify we have the expected skills
+ const skillNames = skills.map((s) => s.name)
+ expect(skillNames).toContain("create-mcp-server")
+ expect(skillNames).toContain("create-mode")
+
+ // Verify each skill has valid content
+ for (const skill of skills) {
+ expect(skill.source).toBe("built-in")
+ expect(skill.path).toBe("built-in")
+
+ const content = getBuiltInSkillContent(skill.name)
+ expect(content).not.toBeNull()
+ expect(content!.instructions.length).toBeGreaterThan(0)
+ }
+ })
+
+ it("should return null for non-existent skills", async () => {
+ const { getBuiltInSkillContent } = await import("../built-in-skills")
+
+ const content = getBuiltInSkillContent("non-existent-skill")
+ expect(content).toBeNull()
+ })
+})
diff --git a/src/core/prompts/instructions/create-mcp-server.ts b/src/services/skills/built-in-skills.ts
similarity index 61%
rename from src/core/prompts/instructions/create-mcp-server.ts
rename to src/services/skills/built-in-skills.ts
index a63fad1de56..a47092b38b4 100644
--- a/src/core/prompts/instructions/create-mcp-server.ts
+++ b/src/services/skills/built-in-skills.ts
@@ -1,17 +1,31 @@
-import { McpHub } from "../../../services/mcp/McpHub"
-import { DiffStrategy } from "../../../shared/tools"
-
-export async function createMCPServerInstructions(
- mcpHub: McpHub | undefined,
- diffStrategy: DiffStrategy | undefined,
-): Promise {
- if (!diffStrategy || !mcpHub) throw new Error("Missing MCP Hub or Diff Strategy")
+/**
+ * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
+ *
+ * This file is generated by generate-built-in-skills.ts from the SKILL.md files
+ * in the built-in/ directory. To modify built-in skills, edit the corresponding
+ * SKILL.md file and run: pnpm generate:skills
+ *
+ * Generated at: 2026-01-28T23:09:14.137Z
+ */
+
+import { SkillMetadata, SkillContent } from "../../shared/skills"
+
+interface BuiltInSkillDefinition {
+ name: string
+ description: string
+ instructions: string
+}
- return `You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`.
+const BUILT_IN_SKILLS: Record = {
+ "create-mcp-server": {
+ name: "create-mcp-server",
+ description:
+ "Instructions for creating MCP (Model Context Protocol) servers that expose tools and resources for the agent to use. Use when the user asks to create a new MCP server or add MCP capabilities.",
+ instructions: `You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`.
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
-Unless the user specifies otherwise, new local MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
+Unless the user specifies otherwise, new local MCP servers should be created in your MCP servers directory. You can find the path to this directory by checking the MCP settings file, or ask the user where they'd like the server created.
### MCP Server Types and Configuration
@@ -58,10 +72,10 @@ For example, if the user wanted to give you the ability to retrieve weather info
The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
-1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
+1. Use the \`create-typescript-server\` tool to bootstrap a new project in your MCP servers directory:
\`\`\`bash
-cd ${await mcpHub.getMcpServersPath()}
+cd /path/to/your/mcp-servers
npx @modelcontextprotocol/create-server weather-server
cd weather-server
# Install dependencies
@@ -77,7 +91,7 @@ weather-server/
...
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
"scripts": {
- "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
+ "build": "tsc && node -e \\"require('fs').chmodSync('build/index.js', '755')\\"",
...
}
...
@@ -275,7 +289,7 @@ npm run build
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
-5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
+5. Install the MCP Server by adding the MCP server configuration to the MCP settings file. On macOS/Linux this is typically at \`~/.roo-code/settings/mcp_settings.json\`, on Windows at \`%APPDATA%\\roo-code\\settings\\mcp_settings.json\`. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[].
@@ -294,7 +308,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
}
\`\`\`
-(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
+(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
@@ -302,14 +316,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
## Editing MCP Servers
-The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${(() => {
- if (!mcpHub) return "(None running currently)"
- const servers = mcpHub
- .getServers()
- .map((server) => server.name)
- .join(", ")
- return servers || "(None running currently)"
- })()}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
+The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' in the system prompt), e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file or apply_diff to make changes to the files.
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
@@ -317,5 +324,105 @@ However some MCP servers may be running from installed packages rather than a lo
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
-Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
+Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`,
+ },
+ "create-mode": {
+ name: "create-mode",
+ description:
+ "Instructions for creating custom modes in Roo Code. Use when the user asks to create a new mode, edit an existing mode, or configure mode settings.",
+ instructions: `Custom modes can be configured in two ways:
+
+1. Globally via the custom modes file in your Roo Code settings directory (typically ~/.roo-code/settings/custom_modes.yaml on macOS/Linux or %APPDATA%\\roo-code\\settings\\custom_modes.yaml on Windows) - created automatically on startup
+2. Per-workspace via '.roomodes' in the workspace root directory
+
+When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes.
+
+If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file.
+
+- The following fields are required and must not be empty:
+
+ - slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
+ - name: The display name for the mode
+ - roleDefinition: A detailed description of the mode's role and capabilities
+ - groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
+
+- The following fields are optional but highly recommended:
+
+ - description: A short, human-readable description of what this mode does (5 words)
+ - whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions.
+ - customInstructions: Additional instructions for how the mode should operate
+
+- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break."
+
+Both files should follow this structure (in YAML format):
+
+customModes:
+
+- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
+ name: Designer # Required: mode display name
+ description: UI/UX design systems expert # Optional but recommended: short description (5 words)
+ roleDefinition: >-
+ You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
+ - Creating and maintaining design systems
+ - Implementing responsive and accessible web interfaces
+ - Working with CSS, HTML, and modern frontend frameworks
+ - Ensuring consistent user experiences across platforms # Required: non-empty
+ whenToUse: >-
+ Use this mode when creating or modifying UI components, implementing design systems,
+ or ensuring responsive web interfaces. This mode is especially effective with CSS,
+ HTML, and modern frontend frameworks. # Optional but recommended
+ groups: # Required: array of tool groups (can be empty)
+ - read # Read files group (read_file, search_files, list_files, codebase_search)
+ - edit # Edit files group (apply_diff, write_to_file) - allows editing any file
+ # Or with file restrictions:
+ # - - edit
+ # - fileRegex: \\.md$
+ # description: Markdown files only # Edit group that only allows editing markdown files
+ - browser # Browser group (browser_action)
+ - command # Command group (execute_command)
+ - mcp # MCP group (use_mcp_tool, access_mcp_resource)
+ customInstructions: Additional instructions for the Designer mode # Optional`,
+ },
+}
+
+/**
+ * Get all built-in skills as SkillMetadata objects
+ */
+export function getBuiltInSkills(): SkillMetadata[] {
+ return Object.values(BUILT_IN_SKILLS).map((skill) => ({
+ name: skill.name,
+ description: skill.description,
+ path: "built-in",
+ source: "built-in" as const,
+ }))
+}
+
+/**
+ * Get a specific built-in skill's full content by name
+ */
+export function getBuiltInSkillContent(name: string): SkillContent | null {
+ const skill = BUILT_IN_SKILLS[name]
+ if (!skill) return null
+
+ return {
+ name: skill.name,
+ description: skill.description,
+ path: "built-in",
+ source: "built-in" as const,
+ instructions: skill.instructions,
+ }
+}
+
+/**
+ * Check if a skill name is a built-in skill
+ */
+export function isBuiltInSkill(name: string): boolean {
+ return name in BUILT_IN_SKILLS
+}
+
+/**
+ * Get names of all built-in skills
+ */
+export function getBuiltInSkillNames(): string[] {
+ return Object.keys(BUILT_IN_SKILLS)
}
diff --git a/src/services/skills/built-in/create-mcp-server/SKILL.md b/src/services/skills/built-in/create-mcp-server/SKILL.md
new file mode 100644
index 00000000000..be52e91c890
--- /dev/null
+++ b/src/services/skills/built-in/create-mcp-server/SKILL.md
@@ -0,0 +1,304 @@
+---
+name: create-mcp-server
+description: Instructions for creating MCP (Model Context Protocol) servers that expose tools and resources for the agent to use. Use when the user asks to create a new MCP server or add MCP capabilities.
+---
+
+You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`.
+
+When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
+
+Unless the user specifies otherwise, new local MCP servers should be created in your MCP servers directory. You can find the path to this directory by checking the MCP settings file, or ask the user where they'd like the server created.
+
+### MCP Server Types and Configuration
+
+MCP servers can be configured in two ways in the MCP settings file:
+
+1. Local (Stdio) Server Configuration:
+
+```json
+{
+ "mcpServers": {
+ "local-weather": {
+ "command": "node",
+ "args": ["/path/to/weather-server/build/index.js"],
+ "env": {
+ "OPENWEATHER_API_KEY": "your-api-key"
+ }
+ }
+ }
+}
+```
+
+2. Remote (SSE) Server Configuration:
+
+```json
+{
+ "mcpServers": {
+ "remote-weather": {
+ "url": "https://api.example.com/mcp",
+ "headers": {
+ "Authorization": "Bearer your-api-key"
+ }
+ }
+ }
+}
+```
+
+Common configuration options for both types:
+
+- `disabled`: (optional) Set to true to temporarily disable the server
+- `timeout`: (optional) Maximum time in seconds to wait for server responses (default: 60)
+- `alwaysAllow`: (optional) Array of tool names that don't require user confirmation
+- `disabledTools`: (optional) Array of tool names that are not included in the system prompt and won't be used
+
+### Example Local MCP Server
+
+For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
+
+The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
+
+1. Use the `create-typescript-server` tool to bootstrap a new project in your MCP servers directory:
+
+```bash
+cd /path/to/your/mcp-servers
+npx @modelcontextprotocol/create-server weather-server
+cd weather-server
+# Install dependencies
+npm install axios zod @modelcontextprotocol/sdk
+```
+
+This will create a new project with the following structure:
+
+```
+weather-server/
+ ├── package.json
+ {
+ ...
+ "type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
+ "scripts": {
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
+ ...
+ }
+ ...
+ }
+ ├── tsconfig.json
+ └── src/
+ └── index.ts # Main server implementation
+```
+
+2. Replace `src/index.ts` with the following:
+
+```typescript
+#!/usr/bin/env node
+import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
+import { z } from "zod"
+import axios from "axios"
+
+const API_KEY = process.env.OPENWEATHER_API_KEY // provided by MCP config
+if (!API_KEY) {
+ throw new Error("OPENWEATHER_API_KEY environment variable is required")
+}
+
+// Define types for OpenWeather API responses
+interface WeatherData {
+ main: {
+ temp: number
+ humidity: number
+ }
+ weather: Array<{
+ description: string
+ }>
+ wind: {
+ speed: number
+ }
+}
+
+interface ForecastData {
+ list: Array<
+ WeatherData & {
+ dt_txt: string
+ }
+ >
+}
+
+// Create an MCP server
+const server = new McpServer({
+ name: "weather-server",
+ version: "0.1.0",
+})
+
+// Create axios instance for OpenWeather API
+const weatherApi = axios.create({
+ baseURL: "http://api.openweathermap.org/data/2.5",
+ params: {
+ appid: API_KEY,
+ units: "metric",
+ },
+})
+
+// Add a tool for getting weather forecasts
+server.tool(
+ "get_forecast",
+ {
+ city: z.string().describe("City name"),
+ days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"),
+ },
+ async ({ city, days = 3 }) => {
+ try {
+ const response = await weatherApi.get("forecast", {
+ params: {
+ q: city,
+ cnt: Math.min(days, 5) * 8,
+ },
+ })
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(response.data.list, null, 2),
+ },
+ ],
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Weather API error: ${error.response?.data.message ?? error.message}`,
+ },
+ ],
+ isError: true,
+ }
+ }
+ throw error
+ }
+ },
+)
+
+// Add a resource for current weather in San Francisco
+server.resource("sf_weather", { uri: "weather://San Francisco/current", list: true }, async (uri) => {
+ try {
+ const response = weatherApi.get("weather", {
+ params: { q: "San Francisco" },
+ })
+
+ return {
+ contents: [
+ {
+ uri: uri.href,
+ mimeType: "application/json",
+ text: JSON.stringify(
+ {
+ temperature: response.data.main.temp,
+ conditions: response.data.weather[0].description,
+ humidity: response.data.main.humidity,
+ wind_speed: response.data.wind.speed,
+ timestamp: new Date().toISOString(),
+ },
+ null,
+ 2,
+ ),
+ },
+ ],
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ throw new Error(`Weather API error: ${error.response?.data.message ?? error.message}`)
+ }
+ throw error
+ }
+})
+
+// Add a dynamic resource template for current weather by city
+server.resource(
+ "current_weather",
+ new ResourceTemplate("weather://{city}/current", { list: true }),
+ async (uri, { city }) => {
+ try {
+ const response = await weatherApi.get("weather", {
+ params: { q: city },
+ })
+
+ return {
+ contents: [
+ {
+ uri: uri.href,
+ mimeType: "application/json",
+ text: JSON.stringify(
+ {
+ temperature: response.data.main.temp,
+ conditions: response.data.weather[0].description,
+ humidity: response.data.main.humidity,
+ wind_speed: response.data.wind.speed,
+ timestamp: new Date().toISOString(),
+ },
+ null,
+ 2,
+ ),
+ },
+ ],
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ throw new Error(`Weather API error: ${error.response?.data.message ?? error.message}`)
+ }
+ throw error
+ }
+ },
+)
+
+// Start receiving messages on stdin and sending messages on stdout
+const transport = new StdioServerTransport()
+await server.connect(transport)
+console.error("Weather MCP server running on stdio")
+```
+
+(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
+
+3. Build and compile the executable JavaScript file
+
+```bash
+npm run build
+```
+
+4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
+
+5. Install the MCP Server by adding the MCP server configuration to the MCP settings file. On macOS/Linux this is typically at `~/.roo-code/settings/mcp_settings.json`, on Windows at `%APPDATA%\roo-code\settings\mcp_settings.json`. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing `mcpServers` object.
+
+IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[].
+
+```json
+{
+ "mcpServers": {
+ ...,
+ "weather": {
+ "command": "node",
+ "args": ["/path/to/weather-server/build/index.js"],
+ "env": {
+ "OPENWEATHER_API_KEY": "user-provided-api-key"
+ }
+ },
+ }
+}
+```
+
+(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify `~/Library/Application\ Support/Claude/claude_desktop_config.json` on macOS for example. It follows the same format of a top level `mcpServers` object.)
+
+6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
+
+7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
+
+## Editing MCP Servers
+
+The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' in the system prompt), e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file or apply_diff to make changes to the files.
+
+However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
+
+# MCP Servers Are Not Always Necessary
+
+The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
+
+Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.
diff --git a/src/services/skills/built-in/create-mode/SKILL.md b/src/services/skills/built-in/create-mode/SKILL.md
new file mode 100644
index 00000000000..ec43ac9bea1
--- /dev/null
+++ b/src/services/skills/built-in/create-mode/SKILL.md
@@ -0,0 +1,57 @@
+---
+name: create-mode
+description: Instructions for creating custom modes in Roo Code. Use when the user asks to create a new mode, edit an existing mode, or configure mode settings.
+---
+
+Custom modes can be configured in two ways:
+
+1. Globally via the custom modes file in your Roo Code settings directory (typically ~/.roo-code/settings/custom_modes.yaml on macOS/Linux or %APPDATA%\roo-code\settings\custom_modes.yaml on Windows) - created automatically on startup
+2. Per-workspace via '.roomodes' in the workspace root directory
+
+When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes.
+
+If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file.
+
+- The following fields are required and must not be empty:
+
+ - slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
+ - name: The display name for the mode
+ - roleDefinition: A detailed description of the mode's role and capabilities
+ - groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\.md$", description: "Markdown files only" }] to only allow editing markdown files)
+
+- The following fields are optional but highly recommended:
+
+ - description: A short, human-readable description of what this mode does (5 words)
+ - whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions.
+ - customInstructions: Additional instructions for how the mode should operate
+
+- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break."
+
+Both files should follow this structure (in YAML format):
+
+customModes:
+
+- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
+ name: Designer # Required: mode display name
+ description: UI/UX design systems expert # Optional but recommended: short description (5 words)
+ roleDefinition: >-
+ You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
+ - Creating and maintaining design systems
+ - Implementing responsive and accessible web interfaces
+ - Working with CSS, HTML, and modern frontend frameworks
+ - Ensuring consistent user experiences across platforms # Required: non-empty
+ whenToUse: >-
+ Use this mode when creating or modifying UI components, implementing design systems,
+ or ensuring responsive web interfaces. This mode is especially effective with CSS,
+ HTML, and modern frontend frameworks. # Optional but recommended
+ groups: # Required: array of tool groups (can be empty)
+ - read # Read files group (read_file, search_files, list_files, codebase_search)
+ - edit # Edit files group (apply_diff, write_to_file) - allows editing any file
+ # Or with file restrictions:
+ # - - edit
+ # - fileRegex: \.md$
+ # description: Markdown files only # Edit group that only allows editing markdown files
+ - browser # Browser group (browser_action)
+ - command # Command group (execute_command)
+ - mcp # MCP group (use_mcp_tool, access_mcp_resource)
+ customInstructions: Additional instructions for the Designer mode # Optional
diff --git a/src/services/skills/generate-built-in-skills.ts b/src/services/skills/generate-built-in-skills.ts
new file mode 100644
index 00000000000..a1fb0fcb108
--- /dev/null
+++ b/src/services/skills/generate-built-in-skills.ts
@@ -0,0 +1,302 @@
+#!/usr/bin/env tsx
+/**
+ * Build script to generate built-in-skills.ts from SKILL.md files.
+ *
+ * This script scans the built-in/ directory for skill folders, parses each
+ * SKILL.md file using gray-matter, validates the frontmatter, and generates
+ * the built-in-skills.ts file.
+ *
+ * Run with: npx tsx src/services/skills/generate-built-in-skills.ts
+ */
+
+import * as fs from "fs/promises"
+import * as path from "path"
+import { execSync } from "child_process"
+import matter from "gray-matter"
+
+const BUILT_IN_DIR = path.join(__dirname, "built-in")
+const OUTPUT_FILE = path.join(__dirname, "built-in-skills.ts")
+
+interface SkillData {
+ name: string
+ description: string
+ instructions: string
+}
+
+interface ValidationError {
+ skillDir: string
+ errors: string[]
+}
+
+/**
+ * Validate a skill name according to Agent Skills spec:
+ * - 1-64 characters
+ * - lowercase letters, numbers, and hyphens only
+ * - must not start/end with hyphen
+ * - must not contain consecutive hyphens
+ */
+function validateSkillName(name: string): string[] {
+ const errors: string[] = []
+
+ if (name.length < 1 || name.length > 64) {
+ errors.push(`Name must be 1-64 characters (got ${name.length})`)
+ }
+
+ const nameFormat = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
+ if (!nameFormat.test(name)) {
+ errors.push(
+ "Name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)",
+ )
+ }
+
+ return errors
+}
+
+/**
+ * Validate a skill description:
+ * - 1-1024 characters (after trimming)
+ */
+function validateDescription(description: string): string[] {
+ const errors: string[] = []
+ const trimmed = description.trim()
+
+ if (trimmed.length < 1 || trimmed.length > 1024) {
+ errors.push(`Description must be 1-1024 characters (got ${trimmed.length})`)
+ }
+
+ return errors
+}
+
+/**
+ * Parse and validate a single SKILL.md file
+ */
+async function parseSkillFile(
+ skillDir: string,
+ dirName: string,
+): Promise<{ skill?: SkillData; errors?: ValidationError }> {
+ const skillMdPath = path.join(skillDir, "SKILL.md")
+
+ try {
+ const fileContent = await fs.readFile(skillMdPath, "utf-8")
+ const { data: frontmatter, content: body } = matter(fileContent)
+
+ const errors: string[] = []
+
+ // Validate required fields
+ if (!frontmatter.name || typeof frontmatter.name !== "string") {
+ errors.push("Missing required 'name' field in frontmatter")
+ }
+ if (!frontmatter.description || typeof frontmatter.description !== "string") {
+ errors.push("Missing required 'description' field in frontmatter")
+ }
+
+ if (errors.length > 0) {
+ return { errors: { skillDir, errors } }
+ }
+
+ // Validate name matches directory name
+ if (frontmatter.name !== dirName) {
+ errors.push(`Frontmatter name "${frontmatter.name}" doesn't match directory name "${dirName}"`)
+ }
+
+ // Validate name format
+ errors.push(...validateSkillName(dirName))
+
+ // Validate description
+ errors.push(...validateDescription(frontmatter.description))
+
+ if (errors.length > 0) {
+ return { errors: { skillDir, errors } }
+ }
+
+ return {
+ skill: {
+ name: frontmatter.name,
+ description: frontmatter.description.trim(),
+ instructions: body.trim(),
+ },
+ }
+ } catch (error) {
+ return {
+ errors: {
+ skillDir,
+ errors: [`Failed to read or parse SKILL.md: ${error instanceof Error ? error.message : String(error)}`],
+ },
+ }
+ }
+}
+
+/**
+ * Escape a string for use in TypeScript template literal
+ */
+function escapeForTemplateLiteral(str: string): string {
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
+}
+
+/**
+ * Generate the TypeScript code for built-in-skills.ts
+ */
+function generateTypeScript(skills: Record): string {
+ const skillEntries = Object.entries(skills)
+ .map(([key, skill]) => {
+ const escapedInstructions = escapeForTemplateLiteral(skill.instructions)
+ return `\t"${key}": {
+ name: "${skill.name}",
+ description: "${skill.description.replace(/"/g, '\\"')}",
+ instructions: \`${escapedInstructions}\`,
+ }`
+ })
+ .join(",\n")
+
+ return `/**
+ * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
+ *
+ * This file is generated by generate-built-in-skills.ts from the SKILL.md files
+ * in the built-in/ directory. To modify built-in skills, edit the corresponding
+ * SKILL.md file and run: pnpm generate:skills
+ *
+ * Generated at: ${new Date().toISOString()}
+ */
+
+import { SkillMetadata, SkillContent } from "../../shared/skills"
+
+interface BuiltInSkillDefinition {
+ name: string
+ description: string
+ instructions: string
+}
+
+const BUILT_IN_SKILLS: Record = {
+${skillEntries}
+}
+
+/**
+ * Get all built-in skills as SkillMetadata objects
+ */
+export function getBuiltInSkills(): SkillMetadata[] {
+ return Object.values(BUILT_IN_SKILLS).map((skill) => ({
+ name: skill.name,
+ description: skill.description,
+ path: "built-in",
+ source: "built-in" as const,
+ }))
+}
+
+/**
+ * Get a specific built-in skill's full content by name
+ */
+export function getBuiltInSkillContent(name: string): SkillContent | null {
+ const skill = BUILT_IN_SKILLS[name]
+ if (!skill) return null
+
+ return {
+ name: skill.name,
+ description: skill.description,
+ path: "built-in",
+ source: "built-in" as const,
+ instructions: skill.instructions,
+ }
+}
+
+/**
+ * Check if a skill name is a built-in skill
+ */
+export function isBuiltInSkill(name: string): boolean {
+ return name in BUILT_IN_SKILLS
+}
+
+/**
+ * Get names of all built-in skills
+ */
+export function getBuiltInSkillNames(): string[] {
+ return Object.keys(BUILT_IN_SKILLS)
+}
+`
+}
+
+async function main() {
+ console.log("Generating built-in skills from SKILL.md files...")
+
+ // Check if built-in directory exists
+ try {
+ await fs.access(BUILT_IN_DIR)
+ } catch {
+ console.error(`Error: Built-in skills directory not found: ${BUILT_IN_DIR}`)
+ process.exit(1)
+ }
+
+ // Scan for skill directories
+ const entries = await fs.readdir(BUILT_IN_DIR)
+ const skills: Record = {}
+ const validationErrors: ValidationError[] = []
+
+ for (const entry of entries) {
+ const skillDir = path.join(BUILT_IN_DIR, entry)
+ const stats = await fs.stat(skillDir)
+
+ if (!stats.isDirectory()) {
+ continue
+ }
+
+ // Check if SKILL.md exists
+ const skillMdPath = path.join(skillDir, "SKILL.md")
+ try {
+ await fs.access(skillMdPath)
+ } catch {
+ console.warn(`Warning: No SKILL.md found in ${entry}, skipping`)
+ continue
+ }
+
+ const result = await parseSkillFile(skillDir, entry)
+
+ if (result.errors) {
+ validationErrors.push(result.errors)
+ } else if (result.skill) {
+ skills[entry] = result.skill
+ console.log(` ✓ Parsed ${entry}`)
+ }
+ }
+
+ // Report validation errors
+ if (validationErrors.length > 0) {
+ console.error("\nValidation errors:")
+ for (const { skillDir, errors } of validationErrors) {
+ console.error(`\n ${path.basename(skillDir)}:`)
+ for (const error of errors) {
+ console.error(` - ${error}`)
+ }
+ }
+ process.exit(1)
+ }
+
+ // Check if any skills were found
+ if (Object.keys(skills).length === 0) {
+ console.error("Error: No valid skills found in built-in directory")
+ process.exit(1)
+ }
+
+ // Generate TypeScript
+ const output = generateTypeScript(skills)
+
+ // Write output file
+ await fs.writeFile(OUTPUT_FILE, output, "utf-8")
+
+ // Format with prettier to ensure stable output
+ // Run from workspace root (3 levels up from src/services/skills/) to find .prettierrc.json
+ const workspaceRoot = path.resolve(__dirname, "..", "..", "..")
+ try {
+ execSync(`npx prettier --write "${OUTPUT_FILE}"`, {
+ cwd: workspaceRoot,
+ stdio: "pipe",
+ })
+ console.log(`\n✓ Generated and formatted ${OUTPUT_FILE}`)
+ } catch {
+ console.log(`\n✓ Generated ${OUTPUT_FILE} (prettier not available)`)
+ }
+ console.log(` Skills: ${Object.keys(skills).join(", ")}`)
+}
+
+main().catch((error) => {
+ console.error("Fatal error:", error)
+ process.exit(1)
+})
diff --git a/src/shared/skills.ts b/src/shared/skills.ts
index 7ed85816aa8..ae35b8c3878 100644
--- a/src/shared/skills.ts
+++ b/src/shared/skills.ts
@@ -5,8 +5,8 @@
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
+ path: string // Absolute path to SKILL.md (or "" for built-in skills)
+ source: "global" | "project" | "built-in" // Where the skill was discovered
mode?: string // If set, skill is only available in this mode
}
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index dc1615c0654..5d7435573c8 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -60,6 +60,7 @@ export const toolParamNames = [
"size",
"query",
"args",
+ "skill", // skill tool parameter
"start_line",
"end_line",
"todos",
@@ -103,9 +104,9 @@ export type NativeToolArgs = {
}
browser_action: BrowserActionParams
codebase_search: { query: string; path?: string }
- fetch_instructions: { task: string }
generate_image: GenerateImageParams
run_slash_command: { command: string; args?: string }
+ skill: { skill: string; args?: string }
search_files: { path: string; regex: string; file_pattern?: string | null }
switch_mode: { mode_slug: string; reason: string }
update_todo_list: { todos: string }
@@ -167,11 +168,6 @@ export interface ReadFileToolUse extends ToolUse<"read_file"> {
params: Partial, "args" | "path" | "start_line" | "end_line" | "files">>
}
-export interface FetchInstructionsToolUse extends ToolUse<"fetch_instructions"> {
- name: "fetch_instructions"
- params: Partial, "task">>
-}
-
export interface WriteToFileToolUse extends ToolUse<"write_to_file"> {
name: "write_to_file"
params: Partial, "path" | "content">>
@@ -232,6 +228,11 @@ export interface RunSlashCommandToolUse extends ToolUse<"run_slash_command"> {
params: Partial, "command" | "args">>
}
+export interface SkillToolUse extends ToolUse<"skill"> {
+ name: "skill"
+ params: Partial, "skill" | "args">>
+}
+
export interface GenerateImageToolUse extends ToolUse<"generate_image"> {
name: "generate_image"
params: Partial, "prompt" | "path" | "image">>
@@ -248,7 +249,6 @@ export const TOOL_DISPLAY_NAMES: Record = {
execute_command: "run commands",
read_file: "read files",
read_command_output: "read command output",
- fetch_instructions: "fetch instructions",
write_to_file: "write files",
apply_diff: "apply changes",
search_and_replace: "apply changes using search and replace",
@@ -267,6 +267,7 @@ export const TOOL_DISPLAY_NAMES: Record = {
codebase_search: "codebase search",
update_todo_list: "update todo list",
run_slash_command: "run slash command",
+ skill: "load skill",
generate_image: "generate images",
custom_tool: "use custom tools",
} as const
@@ -274,7 +275,7 @@ export const TOOL_DISPLAY_NAMES: Record = {
// Define available tool groups.
export const TOOL_GROUPS: Record = {
read: {
- tools: ["read_file", "fetch_instructions", "search_files", "list_files", "codebase_search"],
+ tools: ["read_file", "search_files", "list_files", "codebase_search"],
},
edit: {
tools: ["apply_diff", "write_to_file", "generate_image"],
@@ -303,6 +304,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"new_task",
"update_todo_list",
"run_slash_command",
+ "skill",
] as const
/**
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index 6a6d5c3f6df..25bcd61ee3f 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -666,24 +666,75 @@ export const ChatRowContent = ({
>
)
- case "fetchInstructions":
+ case "skill": {
+ const skillInfo = tool
return (
<>
- {toolIcon("file-code")}
- {t("chat:instructions.wantsToFetch")}
+ {toolIcon("book")}
+
+ {message.type === "ask" ? t("chat:skill.wantsToLoad") : t("chat:skill.didLoad")}
+
-
-
+
+
+
+
+ {skillInfo.skill}
+
+ {skillInfo.source && (
+
+ {skillInfo.source}
+
+ )}
+
+
+
+ {isExpanded && (skillInfo.args || skillInfo.description) && (
+
+ {skillInfo.description && (
+
+ {skillInfo.description}
+
+ )}
+ {skillInfo.args && (
+
+ Arguments:
+
+ {skillInfo.args}
+
+
+ )}
+
+ )}
>
)
+ }
case "listFilesTopLevel":
return (
<>
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index 2167ee18b3e..75a9a1a3800 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -1,12 +1,6 @@
import React, { useState } from "react"
import { Trans } from "react-i18next"
-import {
- VSCodeCheckbox,
- VSCodeLink,
- VSCodePanels,
- VSCodePanelTab,
- VSCodePanelView,
-} from "@vscode/webview-ui-toolkit/react"
+import { VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import type { McpServer } from "@roo-code/types"
@@ -35,13 +29,7 @@ import McpEnabledToggle from "./McpEnabledToggle"
import { McpErrorRow } from "./McpErrorRow"
const McpView = () => {
- const {
- mcpServers: servers,
- alwaysAllowMcp,
- mcpEnabled,
- enableMcpServerCreation,
- setEnableMcpServerCreation,
- } = useExtensionState()
+ const { mcpServers: servers, alwaysAllowMcp, mcpEnabled } = useExtensionState()
const { t } = useAppTranslation()
const { isOverThreshold, title, message } = useTooManyTools()
@@ -71,36 +59,6 @@ const McpView = () => {
{mcpEnabled && (
<>
-
-
{
- setEnableMcpServerCreation(e.target.checked)
- vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked })
- }}>
- {t("mcp:enableServerCreation.title")}
-
-
-
-
- Learn about server creation
-
- new
-
-
{t("mcp:enableServerCreation.hint")}
-
-
-
{/* Too Many Tools Warning */}
{isOverThreshold && (
diff --git a/webview-ui/src/components/settings/CreateSkillDialog.tsx b/webview-ui/src/components/settings/CreateSkillDialog.tsx
deleted file mode 100644
index a4daa9989c0..00000000000
--- a/webview-ui/src/components/settings/CreateSkillDialog.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-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
= ({
- 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(MODE_ANY)
- const [nameError, setNameError] = useState(null)
- const [descriptionError, setDescriptionError] = useState(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) => {
- const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "")
- setName(value)
- setNameError(null)
- }, [])
-
- const handleDescriptionChange = useCallback((e: React.ChangeEvent) => {
- setDescription(e.target.value)
- setDescriptionError(null)
- }, [])
-
- 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 (
-
- )
-}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 3edeff9716b..b84a9dd3a3c 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -29,7 +29,6 @@ import {
Users2,
ArrowLeft,
GitCommitVertical,
- Zap,
} from "lucide-react"
import {
@@ -78,7 +77,6 @@ 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"
@@ -101,7 +99,6 @@ export const sectionNames = [
"providers",
"autoApprove",
"slashCommands",
- "skills",
"browser",
"checkpoints",
"notifications",
@@ -519,7 +516,6 @@ const SettingsView = forwardRef(({ 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 },
@@ -810,9 +806,6 @@ const SettingsView = forwardRef(({ onDone, t
{/* Slash Commands Section */}
{renderTab === "slashCommands" && }
- {/* Skills Section */}
- {renderTab === "skills" && }
-
{/* Browser Section */}
{renderTab === "browser" && (
void
- onDelete: () => void
-}
-
-export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) => {
- const { t } = useAppTranslation()
-
- return (
-
- {/* Skill name and description */}
-
-
- {skill.name}
- {skill.mode && (
-
- {skill.mode}
-
- )}
-
- {skill.description && (
-
{skill.description}
- )}
-
-
- {/* Action buttons */}
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/webview-ui/src/components/settings/SkillsSettings.tsx b/webview-ui/src/components/settings/SkillsSettings.tsx
deleted file mode 100644
index b84f02cf32e..00000000000
--- a/webview-ui/src/components/settings/SkillsSettings.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-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(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 (
-
-
{t("settings:sections.skills")}
-
-
- {/* Description section */}
-
-
-
- Docs
-
- ),
- }}
- />
-
-
-
- {/* Project Skills Section - Only show if in a workspace */}
- {hasWorkspace && (
-
-
-
-
-
{t("settings:skills.projectSkills")}
-
-
-
-
- {projectSkills.length > 0 ? (
- projectSkills.map((skill) => (
-
handleEditClick(skill)}
- onDelete={() => handleDeleteClick(skill)}
- />
- ))
- ) : (
-
- {t("settings:skills.noProjectSkills")}
-
- )}
-
-
- )}
-
- {/* Global Skills Section */}
-
-
-
-
-
{t("settings:skills.globalSkills")}
-
-
-
-
- {globalSkills.length > 0 ? (
- globalSkills.map((skill) => (
-
handleEditClick(skill)}
- onDelete={() => handleDeleteClick(skill)}
- />
- ))
- ) : (
-
- {t("settings:skills.noGlobalSkills")}
-
- )}
-
-
-
-
- {/* Delete Confirmation Dialog */}
-
-
-
- {t("settings:skills.deleteDialog.title")}
-
- {t("settings:skills.deleteDialog.description", { name: skillToDelete?.name })}
-
-
-
-
- {t("settings:skills.deleteDialog.cancel")}
-
-
- {t("settings:skills.deleteDialog.confirm")}
-
-
-
-
-
- {/* Create Skill Dialog */}
-
-
- )
-}
diff --git a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx b/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx
deleted file mode 100644
index 096b68b605e..00000000000
--- a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx
+++ /dev/null
@@ -1,404 +0,0 @@
-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) => (
-
- ),
- Dialog: ({ children, open }: any) => (
-
- {open && children}
-
- ),
- DialogContent: ({ children }: any) => {children}
,
- DialogHeader: ({ children }: any) => {children}
,
- DialogTitle: ({ children }: any) => {children}
,
- DialogDescription: ({ children }: any) => {children}
,
- DialogFooter: ({ children }: any) => {children}
,
- Select: ({ children, value, onValueChange }: any) => (
-
- {children}
- onValueChange(e.target.value)}
- />
-
- ),
- SelectTrigger: ({ children }: any) => {children}
,
- SelectValue: ({ placeholder }: any) => {placeholder},
- SelectContent: ({ children }: any) => {children}
,
- SelectItem: ({ children, value }: any) => (
-
- {children}
-
- ),
-}))
-
-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(
- ,
- )
-
- 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(
- ,
- )
-
- expect(screen.getByTestId("dialog")).toHaveAttribute("data-open", "false")
- expect(screen.queryByTestId("dialog-title")).not.toBeInTheDocument()
- })
-
- it("renders name input field", () => {
- render(
- ,
- )
-
- const nameInput = screen.getByPlaceholderText("settings:skills.createDialog.namePlaceholder")
- expect(nameInput).toBeInTheDocument()
- })
-
- it("renders description textarea", () => {
- render(
- ,
- )
-
- const descInput = screen.getByPlaceholderText("settings:skills.createDialog.descriptionPlaceholder")
- expect(descInput).toBeInTheDocument()
- })
-
- it("transforms name input to lowercase with only allowed characters", () => {
- render(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- 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(
- ,
- )
-
- const select = screen.getAllByTestId("select")[0]
- expect(select).toHaveAttribute("data-value", "project")
- })
-
- it("defaults to global source when hasWorkspace is false", () => {
- render(
- ,
- )
-
- const select = screen.getAllByTestId("select")[0]
- expect(select).toHaveAttribute("data-value", "global")
- })
-
- it("renders source selection dropdown", () => {
- render(
- ,
- )
-
- expect(screen.getByTestId("select-item-global")).toBeInTheDocument()
- expect(screen.getByTestId("select-item-project")).toBeInTheDocument()
- })
-
- it("renders mode selection dropdown", () => {
- render(
- ,
- )
-
- // 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(
- ,
- )
-
- 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)
- })
- })
-})
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
index 98f54e0f228..89be961625b 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
@@ -54,31 +54,6 @@ vi.mock("@src/components/ui", () => ({
TooltipProvider: ({ children }: any) => <>{children}>,
TooltipTrigger: ({ children }: any) => <>{children}>,
TooltipContent: ({ children }: any) => {children}
,
- // Add Dialog components (used by CreateSkillDialog)
- Dialog: ({ children, open }: any) => (open ? {children}
: null),
- DialogContent: ({ children, className }: any) => (
-
- {children}
-
- ),
- DialogHeader: ({ children }: any) => {children}
,
- DialogTitle: ({ children }: any) => {children}
,
- DialogDescription: ({ children }: any) => {children}
,
- DialogFooter: ({ children }: any) => {children}
,
- // Add Select components (used by CreateSkillDialog)
- Select: ({ children, value, onValueChange: _onValueChange }: any) => (
-
- {children}
-
- ),
- SelectContent: ({ children }: any) => {children}
,
- SelectItem: ({ children, value }: any) => (
-
- {children}
-
- ),
- SelectTrigger: ({ children }: any) => {children}
,
- SelectValue: ({ placeholder }: any) => {placeholder}
,
}))
// Mock ModesView and McpView since they're rendered during indexing
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx
index b8e18be96de..b80fee3ee10 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx
@@ -212,17 +212,6 @@ vi.mock("@/components/ui", () => ({
CollapsibleContent: ({ children, className }: any) => (
{children}
),
- // Add Dialog components (used by CreateSkillDialog)
- Dialog: ({ children, open }: any) => (open ? {children}
: null),
- DialogContent: ({ children, className }: any) => (
-
- {children}
-
- ),
- DialogHeader: ({ children }: any) => {children}
,
- DialogTitle: ({ children }: any) => {children}
,
- DialogDescription: ({ children }: any) => {children}
,
- DialogFooter: ({ children }: any) => {children}
,
}))
// Mock window.postMessage to trigger state hydration
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
index 9597c21626f..996dad86399 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
@@ -55,31 +55,6 @@ vi.mock("@src/components/ui", () => ({
Popover: ({ children }: any) => <>{children}>,
PopoverTrigger: ({ children }: any) => <>{children}>,
PopoverContent: ({ children }: any) => {children}
,
- // Add Dialog components (used by CreateSkillDialog)
- Dialog: ({ children, open }: any) => (open ? {children}
: null),
- DialogContent: ({ children, className }: any) => (
-
- {children}
-
- ),
- DialogHeader: ({ children }: any) => {children}
,
- DialogTitle: ({ children }: any) => {children}
,
- DialogDescription: ({ children }: any) => {children}
,
- DialogFooter: ({ children }: any) => {children}
,
- // Add Select components (used by CreateSkillDialog)
- Select: ({ children, value, onValueChange: _onValueChange }: any) => (
-
- {children}
-
- ),
- SelectContent: ({ children }: any) => {children}
,
- SelectItem: ({ children, value }: any) => (
-
- {children}
-
- ),
- SelectTrigger: ({ children }: any) => {children}
,
- SelectValue: ({ placeholder }: any) => {placeholder}
,
}))
// Mock ModesView and McpView since they're rendered during indexing
diff --git a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx
deleted file mode 100644
index 4c7eb6596c5..00000000000
--- a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-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) => (
-
- ),
- StandardTooltip: ({ children, content }: any) => (
-
- {children}
-
- ),
-}))
-
-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()
-
- expect(screen.getByText("test-skill")).toBeInTheDocument()
- })
-
- it("renders skill description", () => {
- render()
-
- expect(screen.getByText("A test skill description")).toBeInTheDocument()
- })
-
- it("renders mode badge when skill has mode", () => {
- render()
-
- expect(screen.getByText("architect")).toBeInTheDocument()
- })
-
- it("does not render mode badge when skill has no mode", () => {
- render()
-
- // 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()
-
- 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()
-
- 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()
-
- 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()
-
- expect(screen.getByText("no-desc-skill")).toBeInTheDocument()
- // Description div should not be rendered when empty
- expect(screen.queryByText("A test skill description")).not.toBeInTheDocument()
- })
-
- it("renders with proper styling classes", () => {
- const { container } = render()
-
- const itemDiv = container.firstChild
- expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground")
- })
-
- it("renders both edit and delete buttons", () => {
- render()
-
- const buttons = screen.getAllByTestId("button")
- expect(buttons).toHaveLength(2)
- })
-})
diff --git a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx
deleted file mode 100644
index 1eba7c14795..00000000000
--- a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-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) => (
-
- {open && children}
-
- ),
- AlertDialogContent: ({ children }: any) => {children}
,
- AlertDialogHeader: ({ children }: any) => {children}
,
- AlertDialogTitle: ({ children }: any) => {children}
,
- AlertDialogDescription: ({ children }: any) => {children}
,
- AlertDialogFooter: ({ children }: any) => {children}
,
- AlertDialogAction: ({ children, onClick }: any) => (
-
- ),
- AlertDialogCancel: ({ children, onClick }: any) => (
-
- ),
- Button: ({ children, onClick, disabled, className, variant, size }: any) => (
-
- ),
-}))
-
-// Mock SkillItem component
-vi.mock("../SkillItem", () => ({
- SkillItem: ({ skill, onEdit, onDelete }: any) => (
-
- {skill.name}
- {skill.description && {skill.description}}
- {skill.mode && {skill.mode}}
-
-
-
- ),
-}))
-
-// Mock CreateSkillDialog component
-vi.mock("../CreateSkillDialog", () => ({
- CreateSkillDialog: ({ open, onOpenChange, onSkillCreated }: any) => (
-
- {open && (
- <>
-
-
- >
- )}
-
- ),
-}))
-
-// Mock SectionHeader and Section components
-vi.mock("../SectionHeader", () => ({
- SectionHeader: ({ children }: any) => {children}
,
-}))
-
-vi.mock("../Section", () => ({
- Section: ({ children }: any) => {children}
,
-}))
-
-// Mock SearchableSetting
-vi.mock("../SearchableSetting", () => ({
- SearchableSetting: ({ children }: any) => {children}
,
-}))
-
-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(
-
-
-
-
- ,
- )
-}
-
-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)
- })
-})
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index 1d2c43ff008..9594f83b86a 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -15,7 +15,6 @@ import {
type MarketplaceInstalledMetadata,
type Command,
type McpServer,
- type SkillMetadata,
RouterModels,
ORGANIZATION_ALLOW_ALL,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
@@ -43,7 +42,6 @@ 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
@@ -102,8 +100,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setTerminalOutputPreviewSize: (value: "small" | "medium" | "large") => void
mcpEnabled: boolean
setMcpEnabled: (value: boolean) => void
- enableMcpServerCreation: boolean
- setEnableMcpServerCreation: (value: boolean) => void
remoteControlEnabled: boolean
setRemoteControlEnabled: (value: boolean) => void
taskSyncEnabled: boolean
@@ -213,7 +209,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: 75,
terminalShellIntegrationTimeout: 4000,
mcpEnabled: true,
- enableMcpServerCreation: false,
remoteControlEnabled: false,
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
@@ -280,7 +275,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [filePaths, setFilePaths] = useState([])
const [openedTabs, setOpenedTabs] = useState>([])
const [commands, setCommands] = useState([])
- const [skills, setSkills] = useState([])
const [mcpServers, setMcpServers] = useState([])
const [currentCheckpoint, setCurrentCheckpoint] = useState()
const [extensionRouterModels, setExtensionRouterModels] = useState(undefined)
@@ -379,10 +373,6 @@ 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) => {
@@ -495,7 +485,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
filePaths,
openedTabs,
commands,
- skills,
soundVolume: state.soundVolume,
ttsSpeed: state.ttsSpeed,
writeDelayMs: state.writeDelayMs,
@@ -553,8 +542,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, terminalShellIntegrationDisabled: value })),
setTerminalZdotdir: (value) => setState((prevState) => ({ ...prevState, terminalZdotdir: value })),
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
- setEnableMcpServerCreation: (value) =>
- setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
setRemoteControlEnabled: (value) => setState((prevState) => ({ ...prevState, remoteControlEnabled: value })),
setTaskSyncEnabled: (value) => setState((prevState) => ({ ...prevState, taskSyncEnabled: value }) as any),
setFeatureRoomoteControlEnabled: (value) =>
diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
index 0ee69a4ad68..4c6f395943b 100644
--- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
+++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
@@ -187,7 +187,6 @@ describe("mergeExtensionState", () => {
const baseState: ExtensionState = {
version: "",
mcpEnabled: false,
- enableMcpServerCreation: false,
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json
index c44a8724335..d3653c057d4 100644
--- a/webview-ui/src/i18n/locales/ca/chat.json
+++ b/webview-ui/src/i18n/locales/ca/chat.json
@@ -324,6 +324,10 @@
"description": "S'han eliminat missatges més antics de la conversa per mantenir-se dins del límit de la finestra de context. Aquest és un enfocament ràpid però menys conservador del context en comparació amb la condensació."
}
},
+ "skill": {
+ "wantsToLoad": "En Roo vol carregar una habilitat",
+ "didLoad": "En Roo ha carregat una habilitat"
+ },
"followUpSuggest": {
"copyToInput": "Copiar a l'entrada (o Shift + clic)",
"timerPrefix": "Aprovació automàtica habilitada. Seleccionant en {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json
index 0eef3cb08bb..dfe769e6ea1 100644
--- a/webview-ui/src/i18n/locales/ca/settings.json
+++ b/webview-ui/src/i18n/locales/ca/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Experimental",
"language": "Idioma",
- "about": "Sobre Roo Code",
- "skills": "Skills"
+ "about": "Sobre Roo Code"
},
"about": {
"bugReport": {
@@ -997,50 +996,5 @@
"label": "Requereix {{primaryMod}}+Intro per enviar missatges",
"description": "Quan estigui activat, has de prémer {{primaryMod}}+Intro per enviar missatges en lloc de només Intro"
}
- },
- "skills": {
- "description": "Gestiona les skills que proporcionen instruccions contextuals a l'agent. Les skills s'apliquen automàticament quan són rellevants per a les teves tasques. Més informació",
- "projectSkills": "Skills del Projecte",
- "globalSkills": "Skills Globals",
- "noProjectSkills": "No hi ha skills de projecte configurades. Crea'n una per afegir capacitats específiques del projecte a l'agent.",
- "noGlobalSkills": "No hi ha skills globals configurades. Crea'n una per afegir capacitats a l'agent disponibles en tots els projectes.",
- "addSkill": "Afegir Skill",
- "editSkill": "Editar skill",
- "deleteSkill": "Eliminar skill",
- "deleteDialog": {
- "title": "Eliminar Skill",
- "description": "Estàs segur que vols eliminar la skill \"{{name}}\"? Aquesta acció no es pot desfer.",
- "confirm": "Eliminar",
- "cancel": "Cancel·lar"
- },
- "createDialog": {
- "title": "Crear Nova Skill",
- "description": "Defineix una nova plantilla de skill que proporcioni instruccions contextuals a l'agent.",
- "nameLabel": "Nom",
- "namePlaceholder": "el-meu-nom-de-skill",
- "nameHint": "Només lletres minúscules, números i guions (1-64 caràcters)",
- "descriptionLabel": "Descripció",
- "descriptionPlaceholder": "Descriu quan s'hauria d'utilitzar aquesta skill...",
- "descriptionHint": "Explica què fa aquesta skill i quan l'agent hauria d'aplicar-la (1-1024 caràcters)",
- "sourceLabel": "Ubicació",
- "sourceHint": "Tria si aquesta skill està disponible globalment o només en aquest projecte",
- "modeLabel": "Mode (opcional)",
- "modePlaceholder": "Qualsevol mode",
- "modeHint": "Restringeix aquesta skill a un mode específic",
- "modeAny": "Qualsevol mode",
- "create": "Crear",
- "cancel": "Cancel·lar"
- },
- "source": {
- "global": "Global (disponible en tots els projectes)",
- "project": "Projecte (només aquest espai de treball)"
- },
- "validation": {
- "nameRequired": "El nom és obligatori",
- "nameTooLong": "El nom ha de tenir com a màxim 64 caràcters",
- "nameInvalid": "El nom ha de tenir entre 1 i 64 caràcters, només lletres minúscules, números o guions",
- "descriptionRequired": "La descripció és obligatòria",
- "descriptionTooLong": "La descripció ha de tenir com a màxim 1024 caràcters"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json
index 1f3f11bc81c..e3aefa36298 100644
--- a/webview-ui/src/i18n/locales/de/chat.json
+++ b/webview-ui/src/i18n/locales/de/chat.json
@@ -324,6 +324,10 @@
"description": "Ältere Nachrichten wurden aus der Konversation entfernt, um innerhalb des Kontextfenster-Limits zu bleiben. Dies ist ein schnellerer, aber weniger kontexterhaltender Ansatz im Vergleich zur Komprimierung."
}
},
+ "skill": {
+ "wantsToLoad": "Roo möchte eine Fähigkeit laden",
+ "didLoad": "Roo hat eine Fähigkeit geladen"
+ },
"followUpSuggest": {
"copyToInput": "In Eingabefeld kopieren (oder Shift + Klick)",
"timerPrefix": "Automatische Genehmigung aktiviert. Wähle in {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json
index ba1e2d0f179..c49fac0f3bc 100644
--- a/webview-ui/src/i18n/locales/de/settings.json
+++ b/webview-ui/src/i18n/locales/de/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Experimentell",
"language": "Sprache",
- "about": "Über Roo Code",
- "skills": "Skills"
+ "about": "Über Roo Code"
},
"about": {
"bugReport": {
@@ -997,50 +996,5 @@
"label": "{{primaryMod}}+Enter zum Senden erfordern",
"description": "Wenn aktiviert, musst du {{primaryMod}}+Enter drücken, um Nachrichten zu senden, anstatt nur Enter"
}
- },
- "skills": {
- "description": "Verwalten Sie Skills, die dem Agenten kontextbezogene Anweisungen bereitstellen. Skills werden automatisch angewendet, wenn sie für Ihre Aufgaben relevant sind. Mehr erfahren",
- "projectSkills": "Projekt-Skills",
- "globalSkills": "Globale Skills",
- "noProjectSkills": "Keine Projekt-Skills konfiguriert. Erstellen Sie eine, um projektspezifische Agentenfähigkeiten hinzuzufügen.",
- "noGlobalSkills": "Keine globalen Skills konfiguriert. Erstellen Sie eine, um Agentenfähigkeiten hinzuzufügen, die in allen Projekten verfügbar sind.",
- "addSkill": "Skill hinzufügen",
- "editSkill": "Skill bearbeiten",
- "deleteSkill": "Skill löschen",
- "deleteDialog": {
- "title": "Skill löschen",
- "description": "Sind Sie sicher, dass Sie die Skill \"{{name}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
- "confirm": "Löschen",
- "cancel": "Abbrechen"
- },
- "createDialog": {
- "title": "Neue Skill erstellen",
- "description": "Definieren Sie eine neue Skill-Vorlage, die dem Agenten kontextbezogene Anweisungen bereitstellt.",
- "nameLabel": "Name",
- "namePlaceholder": "mein-skill-name",
- "nameHint": "Nur Kleinbuchstaben, Zahlen und Bindestriche (1-64 Zeichen)",
- "descriptionLabel": "Beschreibung",
- "descriptionPlaceholder": "Beschreiben Sie, wann diese Skill verwendet werden sollte...",
- "descriptionHint": "Erklären Sie, was diese Skill tut und wann der Agent sie anwenden sollte (1-1024 Zeichen)",
- "sourceLabel": "Standort",
- "sourceHint": "Wählen Sie, ob diese Skill global oder nur in diesem Projekt verfügbar ist",
- "modeLabel": "Modus (optional)",
- "modePlaceholder": "Beliebiger Modus",
- "modeHint": "Beschränken Sie diese Skill auf einen bestimmten Modus",
- "modeAny": "Beliebiger Modus",
- "create": "Erstellen",
- "cancel": "Abbrechen"
- },
- "source": {
- "global": "Global (in allen Projekten verfügbar)",
- "project": "Projekt (nur dieser Arbeitsbereich)"
- },
- "validation": {
- "nameRequired": "Name ist erforderlich",
- "nameTooLong": "Name darf höchstens 64 Zeichen lang sein",
- "nameInvalid": "Name muss 1-64 Kleinbuchstaben, Zahlen oder Bindestriche enthalten",
- "descriptionRequired": "Beschreibung ist erforderlich",
- "descriptionTooLong": "Beschreibung darf höchstens 1024 Zeichen lang sein"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json
index d167a19ff3e..7c2d811021a 100644
--- a/webview-ui/src/i18n/locales/en/chat.json
+++ b/webview-ui/src/i18n/locales/en/chat.json
@@ -203,8 +203,9 @@
"description": "Older messages were removed from the conversation to stay within the context window limit. This is a fast but less context-preserving approach compared to condensation."
}
},
- "instructions": {
- "wantsToFetch": "Roo wants to fetch detailed instructions to assist with the current task"
+ "skill": {
+ "wantsToLoad": "Roo wants to load a skill",
+ "didLoad": "Roo loaded a skill"
},
"fileOperations": {
"wantsToRead": "Roo wants to read this file",
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json
index 98cb1e6bf3e..63f4056d666 100644
--- a/webview-ui/src/i18n/locales/en/settings.json
+++ b/webview-ui/src/i18n/locales/en/settings.json
@@ -30,7 +30,6 @@
"modes": "Modes",
"mcp": "MCP Servers",
"worktrees": "Worktrees",
- "skills": "Skills",
"autoApprove": "Auto-Approve",
"browser": "Browser",
"checkpoints": "Checkpoints",
@@ -71,51 +70,6 @@
"slashCommands": {
"description": "Manage your slash commands to quickly execute custom workflows and actions. Learn more"
},
- "skills": {
- "description": "Manage skills that provide contextual instructions to the agent. Skills are automatically applied when relevant to your tasks. Learn more",
- "projectSkills": "Project Skills",
- "globalSkills": "Global Skills",
- "noProjectSkills": "No project skills configured. Create one to add project-specific agent capabilities.",
- "noGlobalSkills": "No global skills configured. Create one to add agent capabilities available across all projects.",
- "addSkill": "Add Skill",
- "editSkill": "Edit skill",
- "deleteSkill": "Delete skill",
- "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",
diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json
index 2c9418cfa7e..d06d55cdfa5 100644
--- a/webview-ui/src/i18n/locales/es/chat.json
+++ b/webview-ui/src/i18n/locales/es/chat.json
@@ -324,6 +324,10 @@
"description": "Se eliminaron mensajes más antiguos de la conversación para mantenerse dentro del límite de la ventana de contexto. Este es un enfoque rápido pero menos conservador del contexto en comparación con la condensación."
}
},
+ "skill": {
+ "wantsToLoad": "Roo quiere cargar una habilidad",
+ "didLoad": "Roo cargó una habilidad"
+ },
"followUpSuggest": {
"copyToInput": "Copiar a la entrada (o Shift + clic)",
"timerPrefix": "Aprobación automática habilitada. Seleccionando en {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json
index 76a4ba509f5..7115da67951 100644
--- a/webview-ui/src/i18n/locales/es/settings.json
+++ b/webview-ui/src/i18n/locales/es/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Experimental",
"language": "Idioma",
- "about": "Acerca de Roo Code",
- "skills": "Skills"
+ "about": "Acerca de Roo Code"
},
"about": {
"bugReport": {
@@ -997,50 +996,5 @@
"label": "Requerir {{primaryMod}}+Enter para enviar mensajes",
"description": "Cuando está activado, debes presionar {{primaryMod}}+Enter para enviar mensajes en lugar de solo Enter"
}
- },
- "skills": {
- "description": "Gestiona skills que proporcionan instrucciones contextuales al agente. Las skills se aplican automáticamente cuando son relevantes para tus tareas. Más información",
- "projectSkills": "Skills del Proyecto",
- "globalSkills": "Skills Globales",
- "noProjectSkills": "No hay skills de proyecto configuradas. Crea una para añadir capacidades específicas del proyecto al agente.",
- "noGlobalSkills": "No hay skills globales configuradas. Crea una para añadir capacidades al agente disponibles en todos los proyectos.",
- "addSkill": "Añadir Skill",
- "editSkill": "Editar skill",
- "deleteSkill": "Eliminar skill",
- "deleteDialog": {
- "title": "Eliminar Skill",
- "description": "¿Estás seguro de que quieres eliminar la skill \"{{name}}\"? Esta acción no se puede deshacer.",
- "confirm": "Eliminar",
- "cancel": "Cancelar"
- },
- "createDialog": {
- "title": "Crear Nueva Skill",
- "description": "Define una nueva plantilla de skill que proporcione instrucciones contextuales al agente.",
- "nameLabel": "Nombre",
- "namePlaceholder": "mi-nombre-de-skill",
- "nameHint": "Solo letras minúsculas, números y guiones (1-64 caracteres)",
- "descriptionLabel": "Descripción",
- "descriptionPlaceholder": "Describe cuándo debería usarse esta skill...",
- "descriptionHint": "Explica qué hace esta skill y cuándo el agente debería aplicarla (1-1024 caracteres)",
- "sourceLabel": "Ubicación",
- "sourceHint": "Elige si esta skill está disponible globalmente o solo en este proyecto",
- "modeLabel": "Modo (opcional)",
- "modePlaceholder": "Cualquier modo",
- "modeHint": "Restringe esta skill a un modo específico",
- "modeAny": "Cualquier modo",
- "create": "Crear",
- "cancel": "Cancelar"
- },
- "source": {
- "global": "Global (disponible en todos los proyectos)",
- "project": "Proyecto (solo este espacio de trabajo)"
- },
- "validation": {
- "nameRequired": "El nombre es obligatorio",
- "nameTooLong": "El nombre debe tener como máximo 64 caracteres",
- "nameInvalid": "El nombre debe tener entre 1 y 64 caracteres, solo letras minúsculas, números o guiones",
- "descriptionRequired": "La descripción es obligatoria",
- "descriptionTooLong": "La descripción debe tener como máximo 1024 caracteres"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json
index 8aa09075dc3..96781dfb712 100644
--- a/webview-ui/src/i18n/locales/fr/chat.json
+++ b/webview-ui/src/i18n/locales/fr/chat.json
@@ -324,6 +324,10 @@
"description": "Les messages plus anciens ont été supprimés de la conversation pour rester dans la limite de la fenêtre de contexte. C'est une approche rapide mais moins conservatrice du contexte par rapport à la condensation."
}
},
+ "skill": {
+ "wantsToLoad": "Roo veut charger une compétence",
+ "didLoad": "Roo a chargé une compétence"
+ },
"followUpSuggest": {
"copyToInput": "Copier vers l'entrée (ou Shift + clic)",
"timerPrefix": "Approbation automatique activée. Sélection dans {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json
index 241b6db5551..8cdbc1edb4f 100644
--- a/webview-ui/src/i18n/locales/fr/settings.json
+++ b/webview-ui/src/i18n/locales/fr/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Expérimental",
"language": "Langue",
- "about": "À propos de Roo Code",
- "skills": "Skills"
+ "about": "À propos de Roo Code"
},
"about": {
"bugReport": {
@@ -997,50 +996,5 @@
"label": "Exiger {{primaryMod}}+Entrée pour envoyer les messages",
"description": "Lorsqu'activé, tu dois appuyer sur {{primaryMod}}+Entrée pour envoyer des messages au lieu de simplement Entrée"
}
- },
- "skills": {
- "description": "Gérez les skills qui fournissent des instructions contextuelles à l'agent. Les skills sont automatiquement appliquées lorsqu'elles sont pertinentes pour vos tâches. En savoir plus",
- "projectSkills": "Skills du Projet",
- "globalSkills": "Skills Globales",
- "noProjectSkills": "Aucune skill de projet configurée. Créez-en une pour ajouter des capacités spécifiques au projet à l'agent.",
- "noGlobalSkills": "Aucune skill globale configurée. Créez-en une pour ajouter des capacités à l'agent disponibles dans tous les projets.",
- "addSkill": "Ajouter une Skill",
- "editSkill": "Modifier la skill",
- "deleteSkill": "Supprimer la skill",
- "deleteDialog": {
- "title": "Supprimer la Skill",
- "description": "Êtes-vous sûr de vouloir supprimer la skill \"{{name}}\" ? Cette action ne peut pas être annulée.",
- "confirm": "Supprimer",
- "cancel": "Annuler"
- },
- "createDialog": {
- "title": "Créer une Nouvelle Skill",
- "description": "Définissez un nouveau modèle de skill qui fournit des instructions contextuelles à l'agent.",
- "nameLabel": "Nom",
- "namePlaceholder": "mon-nom-de-skill",
- "nameHint": "Lettres minuscules, chiffres et tirets uniquement (1-64 caractères)",
- "descriptionLabel": "Description",
- "descriptionPlaceholder": "Décrivez quand cette skill devrait être utilisée...",
- "descriptionHint": "Expliquez ce que fait cette skill et quand l'agent devrait l'appliquer (1-1024 caractères)",
- "sourceLabel": "Emplacement",
- "sourceHint": "Choisissez si cette skill est disponible globalement ou uniquement dans ce projet",
- "modeLabel": "Mode (optionnel)",
- "modePlaceholder": "N'importe quel mode",
- "modeHint": "Restreindre cette skill à un mode spécifique",
- "modeAny": "N'importe quel mode",
- "create": "Créer",
- "cancel": "Annuler"
- },
- "source": {
- "global": "Globale (disponible dans tous les projets)",
- "project": "Projet (cet espace de travail uniquement)"
- },
- "validation": {
- "nameRequired": "Le nom est obligatoire",
- "nameTooLong": "Le nom doit contenir au maximum 64 caractères",
- "nameInvalid": "Le nom doit contenir entre 1 et 64 lettres minuscules, chiffres ou tirets",
- "descriptionRequired": "La description est obligatoire",
- "descriptionTooLong": "La description doit contenir au maximum 1024 caractères"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json
index 9c155e62ec4..225539cd7c1 100644
--- a/webview-ui/src/i18n/locales/hi/chat.json
+++ b/webview-ui/src/i18n/locales/hi/chat.json
@@ -324,6 +324,10 @@
"description": "संदर्भ विंडो सीमा के भीतर रहने के लिए बातचीत से पुराने संदेश हटा दिए गए। संघनन की तुलना में यह एक तेज़ लेकिन कम संदर्भ-संरक्षित दृष्टिकोण है।"
}
},
+ "skill": {
+ "wantsToLoad": "Roo एक कौशल लोड करना चाहता है",
+ "didLoad": "Roo ने एक कौशल लोड किया"
+ },
"followUpSuggest": {
"copyToInput": "इनपुट में कॉपी करें (या Shift + क्लिक)",
"timerPrefix": "ऑटो-अनुमोदन सक्षम है। {{seconds}}s में चयन किया जा रहा है…"
diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json
index 28a3ebec8b2..3f7e0dfa9ab 100644
--- a/webview-ui/src/i18n/locales/hi/settings.json
+++ b/webview-ui/src/i18n/locales/hi/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "प्रायोगिक",
"language": "भाषा",
- "about": "परिचय",
- "skills": "Skills"
+ "about": "परिचय"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "संदेश भेजने के लिए {{primaryMod}}+Enter की आवश्यकता है",
"description": "जब सक्षम हो, तो आपको केवल Enter के बजाय संदेश भेजने के लिए {{primaryMod}}+Enter दबाना होगा"
}
- },
- "skills": {
- "description": "Skills का प्रबंधन करें जो एजेंट को संदर्भात्मक निर्देश प्रदान करते हैं। जब आपके कार्यों के लिए प्रासंगिक हों तो Skills स्वचालित रूप से लागू होते हैं। और जानें",
- "projectSkills": "प्रोजेक्ट Skills",
- "globalSkills": "ग्लोबल Skills",
- "noProjectSkills": "कोई प्रोजेक्ट skills कॉन्फ़िगर नहीं किया गया। प्रोजेक्ट-विशिष्ट एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।",
- "noGlobalSkills": "कोई ग्लोबल skills कॉन्फ़िगर नहीं किया गया। सभी प्रोजेक्ट्स में उपलब्ध एजेंट क्षमताएं जोड़ने के लिए एक बनाएं।",
- "addSkill": "Skill जोड़ें",
- "editSkill": "Skill संपादित करें",
- "deleteSkill": "Skill हटाएं",
- "deleteDialog": {
- "title": "Skill हटाएं",
- "description": "क्या आप वाकई skill \"{{name}}\" को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
- "confirm": "हटाएं",
- "cancel": "रद्द करें"
- },
- "createDialog": {
- "title": "नया Skill बनाएं",
- "description": "एक नया skill टेम्पलेट परिभाषित करें जो एजेंट को संदर्भात्मक निर्देश प्रदान करता है।",
- "nameLabel": "नाम",
- "namePlaceholder": "my-skill-name",
- "nameHint": "केवल छोटे अक्षर, संख्याएं और हाइफ़न (1-64 वर्ण)",
- "descriptionLabel": "विवरण",
- "descriptionPlaceholder": "वर्णन करें कि इस skill का उपयोग कब किया जाना चाहिए...",
- "descriptionHint": "समझाएं कि यह skill क्या करता है और एजेंट को इसे कब लागू करना चाहिए (1-1024 वर्ण)",
- "sourceLabel": "स्थान",
- "sourceHint": "चुनें कि यह skill ग्लोबल रूप से उपलब्ध है या केवल इस प्रोजेक्ट में",
- "modeLabel": "मोड (वैकल्पिक)",
- "modePlaceholder": "कोई भी मोड",
- "modeHint": "इस skill को किसी विशिष्ट मोड तक सीमित करें",
- "modeAny": "कोई भी मोड",
- "create": "बनाएं",
- "cancel": "रद्द करें"
- },
- "source": {
- "global": "ग्लोबल (सभी प्रोजेक्ट्स में उपलब्ध)",
- "project": "प्रोजेक्ट (केवल यह वर्कस्पेस)"
- },
- "validation": {
- "nameRequired": "नाम आवश्यक है",
- "nameTooLong": "नाम 64 वर्णों से अधिक नहीं होना चाहिए",
- "nameInvalid": "नाम 1-64 छोटे अक्षर, संख्याएं या हाइफ़न होना चाहिए",
- "descriptionRequired": "विवरण आवश्यक है",
- "descriptionTooLong": "विवरण 1024 वर्णों से अधिक नहीं होना चाहिए"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json
index c8569f36460..437d588c308 100644
--- a/webview-ui/src/i18n/locales/id/chat.json
+++ b/webview-ui/src/i18n/locales/id/chat.json
@@ -206,6 +206,10 @@
"description": "Pesan lama telah dihapus dari percakapan untuk tetap dalam batas jendela konteks. Ini adalah pendekatan yang cepat tetapi kurang mempertahankan konteks dibandingkan dengan kondensasi."
}
},
+ "skill": {
+ "wantsToLoad": "Roo ingin memuat keterampilan",
+ "didLoad": "Roo telah memuat keterampilan"
+ },
"instructions": {
"wantsToFetch": "Roo ingin mengambil instruksi detail untuk membantu tugas saat ini"
},
diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json
index 1a752ccc496..c1505c4ba9f 100644
--- a/webview-ui/src/i18n/locales/id/settings.json
+++ b/webview-ui/src/i18n/locales/id/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Eksperimental",
"language": "Bahasa",
- "about": "Tentang Roo Code",
- "skills": "Skills"
+ "about": "Tentang Roo Code"
},
"about": {
"bugReport": {
@@ -1027,50 +1026,5 @@
"label": "Memerlukan {{primaryMod}}+Enter untuk mengirim pesan",
"description": "Ketika diaktifkan, kamu harus menekan {{primaryMod}}+Enter untuk mengirim pesan alih-alih hanya Enter"
}
- },
- "skills": {
- "description": "Kelola skills yang memberikan instruksi kontekstual kepada agen. Skills diterapkan secara otomatis saat relevan dengan tugas Anda. Pelajari lebih lanjut",
- "projectSkills": "Skills Proyek",
- "globalSkills": "Skills Global",
- "noProjectSkills": "Tidak ada skills proyek yang dikonfigurasi. Buat satu untuk menambahkan kemampuan agen khusus proyek.",
- "noGlobalSkills": "Tidak ada skills global yang dikonfigurasi. Buat satu untuk menambahkan kemampuan agen yang tersedia di semua proyek.",
- "addSkill": "Tambahkan Skill",
- "editSkill": "Edit skill",
- "deleteSkill": "Hapus skill",
- "deleteDialog": {
- "title": "Hapus Skill",
- "description": "Apakah Anda yakin ingin menghapus skill \"{{name}}\"? Tindakan ini tidak dapat dibatalkan.",
- "confirm": "Hapus",
- "cancel": "Batal"
- },
- "createDialog": {
- "title": "Buat Skill Baru",
- "description": "Tentukan template skill baru yang memberikan instruksi kontekstual kepada agen.",
- "nameLabel": "Nama",
- "namePlaceholder": "nama-skill-saya",
- "nameHint": "Hanya huruf kecil, angka, dan tanda hubung (1-64 karakter)",
- "descriptionLabel": "Deskripsi",
- "descriptionPlaceholder": "Jelaskan kapan skill ini harus digunakan...",
- "descriptionHint": "Jelaskan apa yang dilakukan skill ini dan kapan agen harus menerapkannya (1-1024 karakter)",
- "sourceLabel": "Lokasi",
- "sourceHint": "Pilih apakah skill ini tersedia secara global atau hanya di proyek ini",
- "modeLabel": "Mode (opsional)",
- "modePlaceholder": "Mode apa saja",
- "modeHint": "Batasi skill ini ke mode tertentu",
- "modeAny": "Mode apa saja",
- "create": "Buat",
- "cancel": "Batal"
- },
- "source": {
- "global": "Global (tersedia di semua proyek)",
- "project": "Proyek (workspace ini saja)"
- },
- "validation": {
- "nameRequired": "Nama diperlukan",
- "nameTooLong": "Nama harus 64 karakter atau kurang",
- "nameInvalid": "Nama harus 1-64 huruf kecil, angka, atau tanda hubung",
- "descriptionRequired": "Deskripsi diperlukan",
- "descriptionTooLong": "Deskripsi harus 1024 karakter atau kurang"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json
index ac00a6dea09..7396f31a457 100644
--- a/webview-ui/src/i18n/locales/it/chat.json
+++ b/webview-ui/src/i18n/locales/it/chat.json
@@ -324,6 +324,10 @@
"description": "I messaggi più vecchi sono stati rimossi dalla conversazione per rimanere entro il limite della finestra di contesto. Questo è un approccio veloce ma meno conservativo del contesto rispetto alla condensazione."
}
},
+ "skill": {
+ "wantsToLoad": "Roo vuole caricare una competenza",
+ "didLoad": "Roo ha caricato una competenza"
+ },
"followUpSuggest": {
"copyToInput": "Copia nell'input (o Shift + clic)",
"timerPrefix": "Approvazione automatica abilitata. Selezione tra {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json
index 50445c8d972..139de3f16dd 100644
--- a/webview-ui/src/i18n/locales/it/settings.json
+++ b/webview-ui/src/i18n/locales/it/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Sperimentale",
"language": "Lingua",
- "about": "Informazioni su Roo Code",
- "skills": "Skills"
+ "about": "Informazioni su Roo Code"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Richiedi {{primaryMod}}+Invio per inviare messaggi",
"description": "Quando abilitato, devi premere {{primaryMod}}+Invio per inviare messaggi invece di solo Invio"
}
- },
- "skills": {
- "description": "Gestisci le skills che forniscono istruzioni contestuali all'agente. Le skills vengono applicate automaticamente quando rilevanti per le tue attività. Scopri di più",
- "projectSkills": "Skills del Progetto",
- "globalSkills": "Skills Globali",
- "noProjectSkills": "Nessuna skill di progetto configurata. Creane una per aggiungere capacità specifiche del progetto all'agente.",
- "noGlobalSkills": "Nessuna skill globale configurata. Creane una per aggiungere capacità all'agente disponibili in tutti i progetti.",
- "addSkill": "Aggiungi Skill",
- "editSkill": "Modifica skill",
- "deleteSkill": "Elimina skill",
- "deleteDialog": {
- "title": "Elimina Skill",
- "description": "Sei sicuro di voler eliminare la skill \"{{name}}\"? Questa azione non può essere annullata.",
- "confirm": "Elimina",
- "cancel": "Annulla"
- },
- "createDialog": {
- "title": "Crea Nuova Skill",
- "description": "Definisci un nuovo modello di skill che fornisce istruzioni contestuali all'agente.",
- "nameLabel": "Nome",
- "namePlaceholder": "il-mio-nome-skill",
- "nameHint": "Solo lettere minuscole, numeri e trattini (1-64 caratteri)",
- "descriptionLabel": "Descrizione",
- "descriptionPlaceholder": "Descrivi quando questa skill dovrebbe essere utilizzata...",
- "descriptionHint": "Spiega cosa fa questa skill e quando l'agente dovrebbe applicarla (1-1024 caratteri)",
- "sourceLabel": "Posizione",
- "sourceHint": "Scegli se questa skill è disponibile globalmente o solo in questo progetto",
- "modeLabel": "Modalità (opzionale)",
- "modePlaceholder": "Qualsiasi modalità",
- "modeHint": "Limita questa skill a una modalità specifica",
- "modeAny": "Qualsiasi modalità",
- "create": "Crea",
- "cancel": "Annulla"
- },
- "source": {
- "global": "Globale (disponibile in tutti i progetti)",
- "project": "Progetto (solo questo workspace)"
- },
- "validation": {
- "nameRequired": "Il nome è obbligatorio",
- "nameTooLong": "Il nome deve essere di massimo 64 caratteri",
- "nameInvalid": "Il nome deve contenere 1-64 lettere minuscole, numeri o trattini",
- "descriptionRequired": "La descrizione è obbligatoria",
- "descriptionTooLong": "La descrizione deve essere di massimo 1024 caratteri"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json
index 34a494ba23a..913ae45238d 100644
--- a/webview-ui/src/i18n/locales/ja/chat.json
+++ b/webview-ui/src/i18n/locales/ja/chat.json
@@ -324,6 +324,10 @@
"description": "コンテキストウィンドウの制限内に収めるため、古いメッセージが会話から削除されました。これは圧縮と比較して高速ですが、コンテキストの保持性が低いアプローチです。"
}
},
+ "skill": {
+ "wantsToLoad": "Rooはスキルを読み込もうとしています",
+ "didLoad": "Rooはスキルを読み込みました"
+ },
"followUpSuggest": {
"copyToInput": "入力欄にコピー(またはShift + クリック)",
"timerPrefix": "自動承認が有効です。{{seconds}}秒後に選択中…"
diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json
index 20df4a196a5..7ed64983513 100644
--- a/webview-ui/src/i18n/locales/ja/settings.json
+++ b/webview-ui/src/i18n/locales/ja/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "実験的",
"language": "言語",
- "about": "Roo Codeについて",
- "skills": "Skills"
+ "about": "Roo Codeについて"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "メッセージを送信するには{{primaryMod}}+Enterが必要",
"description": "有効にすると、Enterだけでなく{{primaryMod}}+Enterを押してメッセージを送信する必要があります"
}
- },
- "skills": {
- "description": "エージェントにコンテキスト指示を提供するスキルを管理します。スキルはタスクに関連する場合に自動的に適用されます。詳細を見る",
- "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文字以内である必要があります"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json
index 18d0089e340..dca0fb149fd 100644
--- a/webview-ui/src/i18n/locales/ko/chat.json
+++ b/webview-ui/src/i18n/locales/ko/chat.json
@@ -324,6 +324,10 @@
"description": "컨텍스트 윈도우 제한 내에 유지하기 위해 대화에서 오래된 메시지가 제거되었습니다. 이것은 압축에 비해 빠르지만 컨텍스트 보존 능력이 낮은 접근 방식입니다."
}
},
+ "skill": {
+ "wantsToLoad": "Roo가 스킬을 로드하려고 합니다",
+ "didLoad": "Roo가 스킬을 로드했습니다"
+ },
"followUpSuggest": {
"copyToInput": "입력창에 복사 (또는 Shift + 클릭)",
"timerPrefix": "자동 승인 활성화됨. {{seconds}}초 후 선택 중…"
diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json
index ccb3018c620..20bf3858f70 100644
--- a/webview-ui/src/i18n/locales/ko/settings.json
+++ b/webview-ui/src/i18n/locales/ko/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "실험적",
"language": "언어",
- "about": "Roo Code 정보",
- "skills": "Skills"
+ "about": "Roo Code 정보"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "메시지를 보내려면 {{primaryMod}}+Enter가 필요",
"description": "활성화하면 Enter만으로는 안 되고 {{primaryMod}}+Enter를 눌러야 메시지를 보낼 수 있습니다"
}
- },
- "skills": {
- "description": "에이전트에 컨텍스트 지침을 제공하는 스킬을 관리합니다. 스킬은 작업과 관련이 있을 때 자동으로 적용됩니다. 자세히 알아보기",
- "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자 이하여야 합니다"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json
index 5f0f6936194..1995a5f9a14 100644
--- a/webview-ui/src/i18n/locales/nl/chat.json
+++ b/webview-ui/src/i18n/locales/nl/chat.json
@@ -346,6 +346,10 @@
"description": "Oudere berichten zijn uit het gesprek verwijderd om binnen de limiet van het contextvenster te blijven. Dit is een snelle maar minder contextbehoudende aanpak in vergelijking met samenvoeging."
}
},
+ "skill": {
+ "wantsToLoad": "Roo wil een vaardigheid laden",
+ "didLoad": "Roo heeft een vaardigheid geladen"
+ },
"followUpSuggest": {
"copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)",
"timerPrefix": "Automatisch goedkeuren ingeschakeld. Selecteren in {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json
index fc6cf9737d2..25b2a48f648 100644
--- a/webview-ui/src/i18n/locales/nl/settings.json
+++ b/webview-ui/src/i18n/locales/nl/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Experimenteel",
"language": "Taal",
- "about": "Over Roo Code",
- "skills": "Skills"
+ "about": "Over Roo Code"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Vereist {{primaryMod}}+Enter om berichten te versturen",
"description": "Wanneer ingeschakeld, moet je {{primaryMod}}+Enter indrukken om berichten te versturen in plaats van alleen Enter"
}
- },
- "skills": {
- "description": "Beheer skills die contextuele instructies aan de agent verstrekken. Skills worden automatisch toegepast wanneer ze relevant zijn voor uw taken. Meer informatie",
- "projectSkills": "Projectskills",
- "globalSkills": "Globale Skills",
- "noProjectSkills": "Geen projectskills geconfigureerd. Maak er een om projectspecifieke agentmogelijkheden toe te voegen.",
- "noGlobalSkills": "Geen globale skills geconfigureerd. Maak er een om agentmogelijkheden toe te voegen die beschikbaar zijn in alle projecten.",
- "addSkill": "Skill toevoegen",
- "editSkill": "Skill bewerken",
- "deleteSkill": "Skill verwijderen",
- "deleteDialog": {
- "title": "Skill verwijderen",
- "description": "Weet u zeker dat u de skill \"{{name}}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
- "confirm": "Verwijderen",
- "cancel": "Annuleren"
- },
- "createDialog": {
- "title": "Nieuwe Skill maken",
- "description": "Definieer een nieuwe skillsjabloon die contextuele instructies aan de agent verstrekt.",
- "nameLabel": "Naam",
- "namePlaceholder": "mijn-skill-naam",
- "nameHint": "Alleen kleine letters, cijfers en streepjes (1-64 tekens)",
- "descriptionLabel": "Beschrijving",
- "descriptionPlaceholder": "Beschrijf wanneer deze skill moet worden gebruikt...",
- "descriptionHint": "Leg uit wat deze skill doet en wanneer de agent deze moet toepassen (1-1024 tekens)",
- "sourceLabel": "Locatie",
- "sourceHint": "Kies of deze skill globaal beschikbaar is of alleen in dit project",
- "modeLabel": "Modus (optioneel)",
- "modePlaceholder": "Elke modus",
- "modeHint": "Beperk deze skill tot een specifieke modus",
- "modeAny": "Elke modus",
- "create": "Maken",
- "cancel": "Annuleren"
- },
- "source": {
- "global": "Globaal (beschikbaar in alle projecten)",
- "project": "Project (alleen deze workspace)"
- },
- "validation": {
- "nameRequired": "Naam is verplicht",
- "nameTooLong": "Naam moet maximaal 64 tekens zijn",
- "nameInvalid": "Naam moet 1-64 kleine letters, cijfers of streepjes zijn",
- "descriptionRequired": "Beschrijving is verplicht",
- "descriptionTooLong": "Beschrijving moet maximaal 1024 tekens zijn"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json
index fd90a260034..b1e86920bd2 100644
--- a/webview-ui/src/i18n/locales/pl/chat.json
+++ b/webview-ui/src/i18n/locales/pl/chat.json
@@ -324,6 +324,10 @@
"description": "Starsze wiadomości zostały usunięte z konwersacji, aby pozostać w granicach okna kontekstu. To szybsze, ale mniej zachowujące kontekst podejście w porównaniu z kondensacją."
}
},
+ "skill": {
+ "wantsToLoad": "Roo chce załadować umiejętność",
+ "didLoad": "Roo załadował umiejętność"
+ },
"followUpSuggest": {
"copyToInput": "Kopiuj do pola wprowadzania (lub Shift + kliknięcie)",
"timerPrefix": "Automatyczne zatwierdzanie włączone. Zaznaczanie za {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json
index 76345ccc83a..1ed4e59159c 100644
--- a/webview-ui/src/i18n/locales/pl/settings.json
+++ b/webview-ui/src/i18n/locales/pl/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Eksperymentalne",
"language": "Język",
- "about": "O Roo Code",
- "skills": "Skills"
+ "about": "O Roo Code"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Wymagaj {{primaryMod}}+Enter do wysyłania wiadomości",
"description": "Po włączeniu musisz nacisnąć {{primaryMod}}+Enter, aby wysłać wiadomości, zamiast tylko Enter"
}
- },
- "skills": {
- "description": "Zarządzaj umiejętnościami, które dostarczają kontekstowe instrukcje dla agenta. Umiejętności są automatycznie stosowane, gdy są istotne dla Twoich zadań. Dowiedz się więcej",
- "projectSkills": "Umiejętności Projektu",
- "globalSkills": "Umiejętności Globalne",
- "noProjectSkills": "Brak skonfigurowanych umiejętności projektu. Utwórz jedną, aby dodać możliwości agenta specyficzne dla projektu.",
- "noGlobalSkills": "Brak skonfigurowanych umiejętności globalnych. Utwórz jedną, aby dodać możliwości agenta dostępne we wszystkich projektach.",
- "addSkill": "Dodaj Umiejętność",
- "editSkill": "Edytuj umiejętność",
- "deleteSkill": "Usuń umiejętność",
- "deleteDialog": {
- "title": "Usuń Umiejętność",
- "description": "Czy na pewno chcesz usunąć umiejętność \"{{name}}\"? Tej akcji nie można cofnąć.",
- "confirm": "Usuń",
- "cancel": "Anuluj"
- },
- "createDialog": {
- "title": "Utwórz Nową Umiejętność",
- "description": "Zdefiniuj nowy szablon umiejętności, który dostarcza kontekstowe instrukcje dla agenta.",
- "nameLabel": "Nazwa",
- "namePlaceholder": "moja-nazwa-umiejetnosci",
- "nameHint": "Tylko małe litery, cyfry i myślniki (1-64 znaki)",
- "descriptionLabel": "Opis",
- "descriptionPlaceholder": "Opisz, kiedy ta umiejętność powinna być użyta...",
- "descriptionHint": "Wyjaśnij, co robi ta umiejętność i kiedy agent powinien ją zastosować (1-1024 znaki)",
- "sourceLabel": "Lokalizacja",
- "sourceHint": "Wybierz, czy ta umiejętność jest dostępna globalnie, czy tylko w tym projekcie",
- "modeLabel": "Tryb (opcjonalnie)",
- "modePlaceholder": "Dowolny tryb",
- "modeHint": "Ogranicz tę umiejętność do określonego trybu",
- "modeAny": "Dowolny tryb",
- "create": "Utwórz",
- "cancel": "Anuluj"
- },
- "source": {
- "global": "Globalnie (dostępne we wszystkich projektach)",
- "project": "Projekt (tylko ten obszar roboczy)"
- },
- "validation": {
- "nameRequired": "Nazwa jest wymagana",
- "nameTooLong": "Nazwa musi mieć maksymalnie 64 znaki",
- "nameInvalid": "Nazwa musi zawierać 1-64 małe litery, cyfry lub myślniki",
- "descriptionRequired": "Opis jest wymagany",
- "descriptionTooLong": "Opis musi mieć maksymalnie 1024 znaki"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json
index c6fdc35e824..dc3ae3b3816 100644
--- a/webview-ui/src/i18n/locales/pt-BR/chat.json
+++ b/webview-ui/src/i18n/locales/pt-BR/chat.json
@@ -324,6 +324,10 @@
"description": "Mensagens mais antigas foram removidas da conversa para permanecer dentro do limite da janela de contexto. Esta é uma abordagem rápida, mas menos preservadora de contexto em comparação com a condensação."
}
},
+ "skill": {
+ "wantsToLoad": "Roo quer carregar uma habilidade",
+ "didLoad": "Roo carregou uma habilidade"
+ },
"followUpSuggest": {
"copyToInput": "Copiar para entrada (ou Shift + clique)",
"timerPrefix": "Aprovação automática ativada. Selecionando em {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json
index 9563907917e..1d989db3793 100644
--- a/webview-ui/src/i18n/locales/pt-BR/settings.json
+++ b/webview-ui/src/i18n/locales/pt-BR/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Experimental",
"language": "Idioma",
- "about": "Sobre",
- "skills": "Skills"
+ "about": "Sobre"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Requer {{primaryMod}}+Enter para enviar mensagens",
"description": "Quando ativado, você deve pressionar {{primaryMod}}+Enter para enviar mensagens em vez de apenas Enter"
}
- },
- "skills": {
- "description": "Gerencie skills que fornecem instruções contextuais ao agente. As skills são aplicadas automaticamente quando relevantes para suas tarefas. Saiba mais",
- "projectSkills": "Skills do Projeto",
- "globalSkills": "Skills Globais",
- "noProjectSkills": "Nenhuma skill de projeto configurada. Crie uma para adicionar capacidades específicas do projeto ao agente.",
- "noGlobalSkills": "Nenhuma skill global configurada. Crie uma para adicionar capacidades ao agente disponíveis em todos os projetos.",
- "addSkill": "Adicionar Skill",
- "editSkill": "Editar skill",
- "deleteSkill": "Excluir skill",
- "deleteDialog": {
- "title": "Excluir Skill",
- "description": "Tem certeza de que deseja excluir a skill \"{{name}}\"? Esta ação não pode ser desfeita.",
- "confirm": "Excluir",
- "cancel": "Cancelar"
- },
- "createDialog": {
- "title": "Criar Nova Skill",
- "description": "Defina um novo modelo de skill que fornece instruções contextuais ao agente.",
- "nameLabel": "Nome",
- "namePlaceholder": "meu-nome-de-skill",
- "nameHint": "Apenas letras minúsculas, números e hífens (1-64 caracteres)",
- "descriptionLabel": "Descrição",
- "descriptionPlaceholder": "Descreva quando esta skill deve ser usada...",
- "descriptionHint": "Explique o que esta skill faz e quando o agente deve aplicá-la (1-1024 caracteres)",
- "sourceLabel": "Localização",
- "sourceHint": "Escolha se esta skill está disponível globalmente ou apenas neste projeto",
- "modeLabel": "Modo (opcional)",
- "modePlaceholder": "Qualquer modo",
- "modeHint": "Restrinja esta skill a um modo específico",
- "modeAny": "Qualquer modo",
- "create": "Criar",
- "cancel": "Cancelar"
- },
- "source": {
- "global": "Global (disponível em todos os projetos)",
- "project": "Projeto (apenas este workspace)"
- },
- "validation": {
- "nameRequired": "O nome é obrigatório",
- "nameTooLong": "O nome deve ter no máximo 64 caracteres",
- "nameInvalid": "O nome deve ter 1-64 letras minúsculas, números ou hífens",
- "descriptionRequired": "A descrição é obrigatória",
- "descriptionTooLong": "A descrição deve ter no máximo 1024 caracteres"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json
index dffbc64e8d4..ae182d126f6 100644
--- a/webview-ui/src/i18n/locales/ru/chat.json
+++ b/webview-ui/src/i18n/locales/ru/chat.json
@@ -347,6 +347,10 @@
"description": "Более старые сообщения были удалены из разговора, чтобы остаться в пределах контекстного окна. Это быстрый, но менее сохраняющий контекст подход по сравнению со сжатием."
}
},
+ "skill": {
+ "wantsToLoad": "Roo хочет загрузить навык",
+ "didLoad": "Roo загрузил навык"
+ },
"followUpSuggest": {
"copyToInput": "Скопировать во ввод (то же, что shift + клик)",
"timerPrefix": "Автоматическое одобрение включено. Выбор через {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json
index e6f5443725b..5a736ef1eca 100644
--- a/webview-ui/src/i18n/locales/ru/settings.json
+++ b/webview-ui/src/i18n/locales/ru/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Экспериментальное",
"language": "Язык",
- "about": "О Roo Code",
- "skills": "Skills"
+ "about": "О Roo Code"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Требовать {{primaryMod}}+Enter для отправки сообщений",
"description": "Если включено, необходимо нажать {{primaryMod}}+Enter для отправки сообщений вместо простого Enter"
}
- },
- "skills": {
- "description": "Управляйте навыками, которые предоставляют контекстные инструкции агенту. Навыки автоматически применяются, когда они релевантны вашим задачам. Узнать больше",
- "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 символов"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json
index 5d5d93893e3..267eae1dba6 100644
--- a/webview-ui/src/i18n/locales/tr/chat.json
+++ b/webview-ui/src/i18n/locales/tr/chat.json
@@ -325,6 +325,10 @@
"description": "Bağlam penceresi sınırında kalmak için eski mesajlar konuşmadan kaldırıldı. Bu, yoğunlaştırmaya kıyasla hızlı ancak daha az bağlam koruyucu bir yaklaşımdır."
}
},
+ "skill": {
+ "wantsToLoad": "Roo bir beceri yüklemek istiyor",
+ "didLoad": "Roo bir beceri yükledi"
+ },
"followUpSuggest": {
"copyToInput": "Giriş alanına kopyala (veya Shift + tıklama)",
"timerPrefix": "Otomatik onay etkinleştirildi. {{seconds}}s içinde seçim yapılıyor…"
diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json
index bfe7678bcc8..9110f27d6e5 100644
--- a/webview-ui/src/i18n/locales/tr/settings.json
+++ b/webview-ui/src/i18n/locales/tr/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Deneysel",
"language": "Dil",
- "about": "Roo Code Hakkında",
- "skills": "Skills"
+ "about": "Roo Code Hakkında"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Mesaj göndermek için {{primaryMod}}+Enter gerekli",
"description": "Etkinleştirildiğinde, sadece Enter yerine mesaj göndermek için {{primaryMod}}+Enter'a basmalısınız"
}
- },
- "skills": {
- "description": "Ajana bağlamsal talimatlar sağlayan becerileri yönetin. Beceriler, görevlerinizle ilgili olduklarında otomatik olarak uygulanır. Daha fazla bilgi",
- "projectSkills": "Proje Becerileri",
- "globalSkills": "Genel Beceriler",
- "noProjectSkills": "Yapılandırılmış proje becerisi yok. Projeye özgü ajan yetenekleri eklemek için bir tane oluşturun.",
- "noGlobalSkills": "Yapılandırılmış genel beceri yok. Tüm projelerde kullanılabilir ajan yetenekleri eklemek için bir tane oluşturun.",
- "addSkill": "Beceri Ekle",
- "editSkill": "Beceriyi düzenle",
- "deleteSkill": "Beceriyi sil",
- "deleteDialog": {
- "title": "Beceriyi Sil",
- "description": "\"{{name}}\" becerisini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
- "confirm": "Sil",
- "cancel": "İptal"
- },
- "createDialog": {
- "title": "Yeni Beceri Oluştur",
- "description": "Ajana bağlamsal talimatlar sağlayan yeni bir beceri şablonu tanımlayın.",
- "nameLabel": "Ad",
- "namePlaceholder": "benim-beceri-adim",
- "nameHint": "Yalnızca küçük harfler, rakamlar ve kısa çizgiler (1-64 karakter)",
- "descriptionLabel": "Açıklama",
- "descriptionPlaceholder": "Bu becerinin ne zaman kullanılması gerektiğini açıklayın...",
- "descriptionHint": "Bu becerinin ne yaptığını ve ajanın ne zaman uygulaması gerektiğini açıklayın (1-1024 karakter)",
- "sourceLabel": "Konum",
- "sourceHint": "Bu becerinin genel olarak mı yoksa yalnızca bu projede mi kullanılabilir olduğunu seçin",
- "modeLabel": "Mod (isteğe bağlı)",
- "modePlaceholder": "Herhangi bir mod",
- "modeHint": "Bu beceriyi belirli bir modla sınırlayın",
- "modeAny": "Herhangi bir mod",
- "create": "Oluştur",
- "cancel": "İptal"
- },
- "source": {
- "global": "Genel (tüm projelerde kullanılabilir)",
- "project": "Proje (yalnızca bu çalışma alanı)"
- },
- "validation": {
- "nameRequired": "Ad gereklidir",
- "nameTooLong": "Ad en fazla 64 karakter olmalıdır",
- "nameInvalid": "Ad 1-64 küçük harf, rakam veya kısa çizgi olmalıdır",
- "descriptionRequired": "Açıklama gereklidir",
- "descriptionTooLong": "Açıklama en fazla 1024 karakter olmalıdır"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json
index 76191a03cf1..5d37f66203a 100644
--- a/webview-ui/src/i18n/locales/vi/chat.json
+++ b/webview-ui/src/i18n/locales/vi/chat.json
@@ -325,6 +325,10 @@
"description": "Các tin nhắn cũ hơn đã bị xóa khỏi cuộc trò chuyện để giữ trong giới hạn cửa sổ ngữ cảnh. Đây là cách tiếp cận nhanh nhưng ít bảo toàn ngữ cảnh hơn so với cô đọng."
}
},
+ "skill": {
+ "wantsToLoad": "Roo muốn tải một kỹ năng",
+ "didLoad": "Roo đã tải một kỹ năng"
+ },
"followUpSuggest": {
"copyToInput": "Sao chép vào ô nhập liệu (hoặc Shift + nhấp chuột)",
"timerPrefix": "Phê duyệt tự động được bật. Chọn trong {{seconds}}s…"
diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json
index e6c1c6a1eff..14c8904e09f 100644
--- a/webview-ui/src/i18n/locales/vi/settings.json
+++ b/webview-ui/src/i18n/locales/vi/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "Thử nghiệm",
"language": "Ngôn ngữ",
- "about": "Giới thiệu",
- "skills": "Skills"
+ "about": "Giới thiệu"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "Yêu cầu {{primaryMod}}+Enter để gửi tin nhắn",
"description": "Khi được bật, bạn phải nhấn {{primaryMod}}+Enter để gửi tin nhắn thay vì chỉ nhấn Enter"
}
- },
- "skills": {
- "description": "Quản lý các skill cung cấp hướng dẫn theo ngữ cảnh cho agent. Các skill được áp dụng tự động khi chúng liên quan đến nhiệm vụ của bạn. Tìm hiểu thêm",
- "projectSkills": "Skills Dự Án",
- "globalSkills": "Skills Toàn Cục",
- "noProjectSkills": "Không có skill dự án nào được cấu hình. Tạo một skill để thêm khả năng agent cụ thể cho dự án.",
- "noGlobalSkills": "Không có skill toàn cục nào được cấu hình. Tạo một skill để thêm khả năng agent có sẵn trong tất cả các dự án.",
- "addSkill": "Thêm Skill",
- "editSkill": "Chỉnh sửa skill",
- "deleteSkill": "Xóa skill",
- "deleteDialog": {
- "title": "Xóa Skill",
- "description": "Bạn có chắc chắn muốn xóa skill \"{{name}}\" không? Hành động này không thể hoàn tác.",
- "confirm": "Xóa",
- "cancel": "Hủy"
- },
- "createDialog": {
- "title": "Tạo Skill Mới",
- "description": "Xác định một mẫu skill mới cung cấp hướng dẫn theo ngữ cảnh cho agent.",
- "nameLabel": "Tên",
- "namePlaceholder": "ten-skill-cua-toi",
- "nameHint": "Chỉ chữ thường, số và dấu gạch ngang (1-64 ký tự)",
- "descriptionLabel": "Mô tả",
- "descriptionPlaceholder": "Mô tả khi nào nên sử dụng skill này...",
- "descriptionHint": "Giải thích skill này làm gì và khi nào agent nên áp dụng nó (1-1024 ký tự)",
- "sourceLabel": "Vị trí",
- "sourceHint": "Chọn xem skill này có sẵn toàn cục hay chỉ trong dự án này",
- "modeLabel": "Chế độ (tùy chọn)",
- "modePlaceholder": "Bất kỳ chế độ nào",
- "modeHint": "Hạn chế skill này cho một chế độ cụ thể",
- "modeAny": "Bất kỳ chế độ nào",
- "create": "Tạo",
- "cancel": "Hủy"
- },
- "source": {
- "global": "Toàn cục (có sẵn trong tất cả các dự án)",
- "project": "Dự án (chỉ workspace này)"
- },
- "validation": {
- "nameRequired": "Tên là bắt buộc",
- "nameTooLong": "Tên phải có tối đa 64 ký tự",
- "nameInvalid": "Tên phải là 1-64 chữ thường, số hoặc dấu gạch ngang",
- "descriptionRequired": "Mô tả là bắt buộc",
- "descriptionTooLong": "Mô tả phải có tối đa 1024 ký tự"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json
index e63cc5dd08e..8a46b1e8a48 100644
--- a/webview-ui/src/i18n/locales/zh-CN/chat.json
+++ b/webview-ui/src/i18n/locales/zh-CN/chat.json
@@ -325,6 +325,10 @@
"description": "为保持在上下文窗口限制内,已从对话中移除较旧的消息。与压缩相比,这是一种快速但上下文保留较少的方法。"
}
},
+ "skill": {
+ "wantsToLoad": "Roo 想要加载技能",
+ "didLoad": "Roo 加载了技能"
+ },
"followUpSuggest": {
"copyToInput": "复制到输入框(或按住Shift点击)",
"timerPrefix": "自动批准已启用。{{seconds}}秒后选择中…"
diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json
index 7dfa086c547..1b58484ceda 100644
--- a/webview-ui/src/i18n/locales/zh-CN/settings.json
+++ b/webview-ui/src/i18n/locales/zh-CN/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "实验性",
"language": "语言",
- "about": "关于 Roo Code",
- "skills": "Skills"
+ "about": "关于 Roo Code"
},
"about": {
"bugReport": {
@@ -998,50 +997,5 @@
"label": "需要 {{primaryMod}}+Enter 发送消息",
"description": "启用后,必须按 {{primaryMod}}+Enter 发送消息,而不仅仅是 Enter"
}
- },
- "skills": {
- "description": "管理为代理提供上下文指令的技能。技能会在与您的任务相关时自动应用。了解更多",
- "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个字符"
- }
}
}
diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json
index 95a96503baf..a8bce99ffe7 100644
--- a/webview-ui/src/i18n/locales/zh-TW/chat.json
+++ b/webview-ui/src/i18n/locales/zh-TW/chat.json
@@ -203,6 +203,10 @@
"description": "為保持在上下文視窗限制內,已從對話中移除較舊的訊息。與壓縮相比,這是一種快速但上下文保留較少的方法。"
}
},
+ "skill": {
+ "wantsToLoad": "Roo 想要載入技能",
+ "didLoad": "Roo 載入了技能"
+ },
"instructions": {
"wantsToFetch": "Roo 想要取得詳細指示以協助目前工作"
},
diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json
index e4ebbf61715..d18a5c8443f 100644
--- a/webview-ui/src/i18n/locales/zh-TW/settings.json
+++ b/webview-ui/src/i18n/locales/zh-TW/settings.json
@@ -41,8 +41,7 @@
"ui": "UI",
"experimental": "實驗性",
"language": "語言",
- "about": "關於 Roo Code",
- "skills": "Skills"
+ "about": "關於 Roo Code"
},
"about": {
"bugReport": {
@@ -995,60 +994,5 @@
"output": "輸出",
"cacheReads": "快取讀取"
}
- },
- "ui": {
- "collapseThinking": {
- "label": "預設折疊「思考」訊息",
- "description": "啟用後,「思考」塊將預設折疊,直到您與其互動"
- },
- "requireCtrlEnterToSend": {
- "label": "需要 {{primaryMod}}+Enter 傳送訊息",
- "description": "啟用後,必須按 {{primaryMod}}+Enter 傳送訊息,而不只是 Enter"
- }
- },
- "skills": {
- "description": "管理為代理提供上下文指令的技能。技能會在與您的任務相關時自動套用。深入了解",
- "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個字元"
- }
}
}