diff --git a/packages/types/src/skills.ts b/packages/types/src/skills.ts index 3e856612bcd..2f13b822ebb 100644 --- a/packages/types/src/skills.ts +++ b/packages/types/src/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 (or "" for built-in skills) - source: "global" | "project" | "built-in" // Where the skill was discovered + path: string // Absolute path to SKILL.md + source: "global" | "project" // Where the skill was discovered /** * @deprecated Use modeSlugs instead. Kept for backward compatibility. * If set, skill is only available in this mode. diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 45c4ab2090a..a727a251035 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -631,7 +631,7 @@ export interface WebviewMessage { modeConfig?: ModeConfig timeout?: number payload?: WebViewMessagePayload - source?: "global" | "project" | "built-in" + source?: "global" | "project" skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile) /** @deprecated Use skillModeSlugs instead */ skillMode?: string // For skill operations (current mode restriction) diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index f9de2ccfe36..558d4476a81 100644 --- a/src/core/auto-approval/index.ts +++ b/src/core/auto-approval/index.ts @@ -151,7 +151,7 @@ export async function checkAutoApproval({ return { decision: "approve" } } - // The skill tool only loads pre-defined instructions from built-in, global, or project skills. + // The skill tool only loads pre-defined instructions from 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") { diff --git a/src/core/prompts/sections/skills.ts b/src/core/prompts/sections/skills.ts index 39cfca405b5..e34d314faf3 100644 --- a/src/core/prompts/sections/skills.ts +++ b/src/core/prompts/sections/skills.ts @@ -33,10 +33,7 @@ export async function getSkillsSection( .map((skill) => { const name = escapeXml(skill.name) const description = escapeXml(skill.description) - // 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)}` : "" + const locationLine = `\n ${escapeXml(skill.path)}` return ` \n ${name}\n ${description}${locationLine}\n ` }) .join("\n") diff --git a/src/core/tools/__tests__/skillTool.spec.ts b/src/core/tools/__tests__/skillTool.spec.ts index fc1b3396e50..037507c6a5e 100644 --- a/src/core/tools/__tests__/skillTool.spec.ts +++ b/src/core/tools/__tests__/skillTool.spec.ts @@ -99,7 +99,7 @@ describe("skillTool", () => { ) }) - it("should successfully load built-in skill", async () => { + it("should successfully load a global skill", async () => { const block: ToolUse<"skill"> = { type: "tool_use" as const, name: "skill" as const, @@ -113,7 +113,7 @@ describe("skillTool", () => { const mockSkillContent = { name: "create-mcp-server", description: "Instructions for creating MCP servers", - source: "built-in", + source: "global", instructions: "Step 1: Create the server...", } @@ -127,7 +127,7 @@ describe("skillTool", () => { tool: "skill", skill: "create-mcp-server", args: undefined, - source: "built-in", + source: "global", description: "Instructions for creating MCP servers", }), ) @@ -135,7 +135,7 @@ describe("skillTool", () => { expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( `Skill: create-mcp-server Description: Instructions for creating MCP servers -Source: built-in +Source: global --- Skill Instructions --- @@ -158,7 +158,7 @@ Step 1: Create the server...`, const mockSkillContent = { name: "create-mcp-server", description: "Instructions for creating MCP servers", - source: "built-in", + source: "global", instructions: "Step 1: Create the server...", } @@ -170,7 +170,7 @@ Step 1: Create the server...`, `Skill: create-mcp-server Description: Instructions for creating MCP servers Provided arguments: weather API server -Source: built-in +Source: global --- Skill Instructions --- @@ -192,7 +192,7 @@ Step 1: Create the server...`, mockSkillsManager.getSkillContent.mockResolvedValue({ name: "create-mcp-server", description: "Test", - source: "built-in", + source: "global", instructions: "Test instructions", }) @@ -264,7 +264,7 @@ Step 1: Create the server...`, const mockSkillContent = { name: "create-mcp-server", description: "Test", - source: "built-in", + source: "global", instructions: "Test instructions", } diff --git a/src/core/webview/__tests__/skillsMessageHandler.spec.ts b/src/core/webview/__tests__/skillsMessageHandler.spec.ts index cdc571282f2..4aac6929112 100644 --- a/src/core/webview/__tests__/skillsMessageHandler.spec.ts +++ b/src/core/webview/__tests__/skillsMessageHandler.spec.ts @@ -28,7 +28,6 @@ vi.mock("../../../i18n", () => ({ "skills:errors.missing_delete_fields": "Missing required fields: skillName or source", "skills:errors.missing_move_fields": "Missing required fields: skillName or source", "skills:errors.skill_not_found": `Skill "${params?.name}" not found`, - "skills:errors.cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved", } return translations[key] || key }, @@ -333,25 +332,6 @@ describe("skillsMessageHandler", () => { "Failed to move skill: Skills manager not available", ) }) - - it("returns undefined when trying to move a built-in skill", async () => { - const provider = createMockProvider(true) - - const result = await handleMoveSkill(provider, { - type: "moveSkill", - skillName: "test-skill", - source: "built-in", - newSkillMode: "code", - } as WebviewMessage) - - expect(result).toBeUndefined() - expect(mockLog).toHaveBeenCalledWith( - "Error moving skill: Built-in skills cannot be created, deleted, or moved", - ) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Failed to move skill: Built-in skills cannot be created, deleted, or moved", - ) - }) }) describe("handleOpenSkillFile", () => { diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index f5db0473fb2..496ff70c243 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -6,6 +6,8 @@ import type { ClineProvider } from "./ClineProvider" import { openFile } from "../../integrations/misc/open-file" import { t } from "../../i18n" +type SkillSource = SkillMetadata["source"] + /** * Handles the requestSkills message - returns all skills metadata */ @@ -36,7 +38,7 @@ export async function handleCreateSkill( ): Promise { try { const skillName = message.skillName - const source = message.source + const source = message.source as SkillSource const skillDescription = message.skillDescription // Support new modeSlugs array or fall back to legacy skillMode const modeSlugs = message.skillModeSlugs ?? (message.skillMode ? [message.skillMode] : undefined) @@ -45,11 +47,6 @@ export async function handleCreateSkill( throw new Error(t("skills:errors.missing_create_fields")) } - // Built-in skills cannot be created - if (source === "built-in") { - throw new Error(t("skills:errors.cannot_modify_builtin")) - } - const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -81,7 +78,7 @@ export async function handleDeleteSkill( ): Promise { try { const skillName = message.skillName - const source = message.source + const source = message.source as SkillSource // Support new skillModeSlugs array or fall back to legacy skillMode const skillMode = message.skillModeSlugs?.[0] ?? message.skillMode @@ -89,11 +86,6 @@ export async function handleDeleteSkill( throw new Error(t("skills:errors.missing_delete_fields")) } - // Built-in skills cannot be deleted - if (source === "built-in") { - throw new Error(t("skills:errors.cannot_modify_builtin")) - } - const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -122,7 +114,7 @@ export async function handleMoveSkill( ): Promise { try { const skillName = message.skillName - const source = message.source + const source = message.source as SkillSource const currentMode = message.skillMode const newMode = message.newSkillMode @@ -130,11 +122,6 @@ export async function handleMoveSkill( throw new Error(t("skills:errors.missing_move_fields")) } - // Built-in skills cannot be moved - if (source === "built-in") { - throw new Error(t("skills:errors.cannot_modify_builtin")) - } - const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -163,18 +150,13 @@ export async function handleUpdateSkillModes( ): Promise { try { const skillName = message.skillName - const source = message.source + const source = message.source as SkillSource const newModeSlugs = message.newSkillModeSlugs if (!skillName || !source) { throw new Error(t("skills:errors.missing_update_modes_fields")) } - // Built-in skills cannot be modified - if (source === "built-in") { - throw new Error(t("skills:errors.cannot_modify_builtin")) - } - const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -200,17 +182,12 @@ export async function handleUpdateSkillModes( export async function handleOpenSkillFile(provider: ClineProvider, message: WebviewMessage): Promise { try { const skillName = message.skillName - const source = message.source + const source = message.source as SkillSource if (!skillName || !source) { throw new Error(t("skills:errors.missing_delete_fields")) } - // Built-in skills cannot be opened as files (they have no file path) - if (source === "built-in") { - throw new Error(t("skills:errors.cannot_open_builtin")) - } - const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json index 47b59938896..1fb358a350b 100644 --- a/src/i18n/locales/ca/skills.json +++ b/src/i18n/locales/ca/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Falten camps obligatoris: skillName o source", "manager_unavailable": "El gestor d'habilitats no està disponible", "missing_delete_fields": "Falten camps obligatoris: skillName o source", - "skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"", - "cannot_modify_builtin": "Les habilitats integrades no es poden crear ni eliminar", - "cannot_open_builtin": "Les habilitats integrades no es poden obrir com a fitxers" + "skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"" } } diff --git a/src/i18n/locales/de/skills.json b/src/i18n/locales/de/skills.json index fe051288952..9c1107e9bfb 100644 --- a/src/i18n/locales/de/skills.json +++ b/src/i18n/locales/de/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Erforderliche Felder fehlen: skillName oder source", "manager_unavailable": "Skill-Manager nicht verfügbar", "missing_delete_fields": "Erforderliche Felder fehlen: skillName oder source", - "skill_not_found": "Skill \"{{name}}\" nicht gefunden", - "cannot_modify_builtin": "Integrierte Skills können nicht erstellt oder gelöscht werden", - "cannot_open_builtin": "Integrierte Skills können nicht als Dateien geöffnet werden" + "skill_not_found": "Skill \"{{name}}\" nicht gefunden" } } diff --git a/src/i18n/locales/en/skills.json b/src/i18n/locales/en/skills.json index 5b6dde45b98..307b59d3654 100644 --- a/src/i18n/locales/en/skills.json +++ b/src/i18n/locales/en/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Missing required fields: skillName or source", "manager_unavailable": "Skills manager not available", "missing_delete_fields": "Missing required fields: skillName or source", - "skill_not_found": "Skill \"{{name}}\" not found", - "cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved", - "cannot_open_builtin": "Built-in skills cannot be opened as files" + "skill_not_found": "Skill \"{{name}}\" not found" } } diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json index 84ab35b6d12..6e10006effd 100644 --- a/src/i18n/locales/es/skills.json +++ b/src/i18n/locales/es/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Faltan campos obligatorios: skillName o source", "manager_unavailable": "El gestor de habilidades no está disponible", "missing_delete_fields": "Faltan campos obligatorios: skillName o source", - "skill_not_found": "No se encontró la habilidad \"{{name}}\"", - "cannot_modify_builtin": "Las habilidades integradas no se pueden crear ni eliminar", - "cannot_open_builtin": "Las habilidades integradas no se pueden abrir como archivos" + "skill_not_found": "No se encontró la habilidad \"{{name}}\"" } } diff --git a/src/i18n/locales/fr/skills.json b/src/i18n/locales/fr/skills.json index 6320a4f55db..3f2b6ac5296 100644 --- a/src/i18n/locales/fr/skills.json +++ b/src/i18n/locales/fr/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Champs obligatoires manquants : skillName ou source", "manager_unavailable": "Le gestionnaire de compétences n'est pas disponible", "missing_delete_fields": "Champs obligatoires manquants : skillName ou source", - "skill_not_found": "Compétence \"{{name}}\" introuvable", - "cannot_modify_builtin": "Les compétences intégrées ne peuvent pas être créées ou supprimées", - "cannot_open_builtin": "Les compétences intégrées ne peuvent pas être ouvertes en tant que fichiers" + "skill_not_found": "Compétence \"{{name}}\" introuvable" } } diff --git a/src/i18n/locales/hi/skills.json b/src/i18n/locales/hi/skills.json index 9b79cdb30f7..ed04e50b5ea 100644 --- a/src/i18n/locales/hi/skills.json +++ b/src/i18n/locales/hi/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", "manager_unavailable": "स्किल मैनेजर उपलब्ध नहीं है", "missing_delete_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", - "skill_not_found": "स्किल \"{{name}}\" नहीं मिला", - "cannot_modify_builtin": "बिल्ट-इन स्किल्स को बनाया या हटाया नहीं जा सकता", - "cannot_open_builtin": "बिल्ट-इन स्किल्स को फाइलों के रूप में नहीं खोला जा सकता" + "skill_not_found": "स्किल \"{{name}}\" नहीं मिला" } } diff --git a/src/i18n/locales/id/skills.json b/src/i18n/locales/id/skills.json index 6559a9d6b1d..433fe0b0c45 100644 --- a/src/i18n/locales/id/skills.json +++ b/src/i18n/locales/id/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Bidang wajib tidak ada: skillName atau source", "manager_unavailable": "Manajer skill tidak tersedia", "missing_delete_fields": "Bidang wajib tidak ada: skillName atau source", - "skill_not_found": "Skill \"{{name}}\" tidak ditemukan", - "cannot_modify_builtin": "Skill bawaan tidak dapat dibuat atau dihapus", - "cannot_open_builtin": "Skill bawaan tidak dapat dibuka sebagai file" + "skill_not_found": "Skill \"{{name}}\" tidak ditemukan" } } diff --git a/src/i18n/locales/it/skills.json b/src/i18n/locales/it/skills.json index fdfd82e261c..2f363a6cd0a 100644 --- a/src/i18n/locales/it/skills.json +++ b/src/i18n/locales/it/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Campi obbligatori mancanti: skillName o source", "manager_unavailable": "Il gestore delle skill non è disponibile", "missing_delete_fields": "Campi obbligatori mancanti: skillName o source", - "skill_not_found": "Skill \"{{name}}\" non trovata", - "cannot_modify_builtin": "Le skill integrate non possono essere create o eliminate", - "cannot_open_builtin": "Le skill integrate non possono essere aperte come file" + "skill_not_found": "Skill \"{{name}}\" non trovata" } } diff --git a/src/i18n/locales/ja/skills.json b/src/i18n/locales/ja/skills.json index 074baacdfdf..90b44d9c956 100644 --- a/src/i18n/locales/ja/skills.json +++ b/src/i18n/locales/ja/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "必須フィールドが不足しています:skillNameまたはsource", "manager_unavailable": "スキルマネージャーが利用できません", "missing_delete_fields": "必須フィールドが不足しています:skillNameまたはsource", - "skill_not_found": "スキル「{{name}}」が見つかりません", - "cannot_modify_builtin": "組み込みスキルは作成または削除できません", - "cannot_open_builtin": "組み込みスキルはファイルとして開けません" + "skill_not_found": "スキル「{{name}}」が見つかりません" } } diff --git a/src/i18n/locales/ko/skills.json b/src/i18n/locales/ko/skills.json index 5386675ea6e..5e4d59f92cf 100644 --- a/src/i18n/locales/ko/skills.json +++ b/src/i18n/locales/ko/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "필수 필드 누락: skillName 또는 source", "manager_unavailable": "스킬 관리자를 사용할 수 없습니다", "missing_delete_fields": "필수 필드 누락: skillName 또는 source", - "skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다", - "cannot_modify_builtin": "기본 제공 스킬은 생성하거나 삭제할 수 없습니다", - "cannot_open_builtin": "기본 제공 스킬은 파일로 열 수 없습니다" + "skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다" } } diff --git a/src/i18n/locales/nl/skills.json b/src/i18n/locales/nl/skills.json index ed9caab43b4..4ca83f1a35c 100644 --- a/src/i18n/locales/nl/skills.json +++ b/src/i18n/locales/nl/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Vereiste velden ontbreken: skillName of source", "manager_unavailable": "Vaardigheidenbeheerder niet beschikbaar", "missing_delete_fields": "Vereiste velden ontbreken: skillName of source", - "skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden", - "cannot_modify_builtin": "Ingebouwde vaardigheden kunnen niet worden aangemaakt of verwijderd", - "cannot_open_builtin": "Ingebouwde vaardigheden kunnen niet als bestanden worden geopend" + "skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden" } } diff --git a/src/i18n/locales/pl/skills.json b/src/i18n/locales/pl/skills.json index 7a5e5f0ac18..93927d1d149 100644 --- a/src/i18n/locales/pl/skills.json +++ b/src/i18n/locales/pl/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Brakuje wymaganych pól: skillName lub source", "manager_unavailable": "Menedżer umiejętności niedostępny", "missing_delete_fields": "Brakuje wymaganych pól: skillName lub source", - "skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"", - "cannot_modify_builtin": "Wbudowane umiejętności nie mogą być tworzone ani usuwane", - "cannot_open_builtin": "Wbudowane umiejętności nie mogą być otwierane jako pliki" + "skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"" } } diff --git a/src/i18n/locales/pt-BR/skills.json b/src/i18n/locales/pt-BR/skills.json index eac683e7fe7..2a0881bd8f6 100644 --- a/src/i18n/locales/pt-BR/skills.json +++ b/src/i18n/locales/pt-BR/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Campos obrigatórios ausentes: skillName ou source", "manager_unavailable": "Gerenciador de habilidades não disponível", "missing_delete_fields": "Campos obrigatórios ausentes: skillName ou source", - "skill_not_found": "Habilidade \"{{name}}\" não encontrada", - "cannot_modify_builtin": "Habilidades integradas não podem ser criadas ou excluídas", - "cannot_open_builtin": "Habilidades integradas não podem ser abertas como arquivos" + "skill_not_found": "Habilidade \"{{name}}\" não encontrada" } } diff --git a/src/i18n/locales/ru/skills.json b/src/i18n/locales/ru/skills.json index 740d813873d..c505d51de73 100644 --- a/src/i18n/locales/ru/skills.json +++ b/src/i18n/locales/ru/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Отсутствуют обязательные поля: skillName или source", "manager_unavailable": "Менеджер навыков недоступен", "missing_delete_fields": "Отсутствуют обязательные поля: skillName или source", - "skill_not_found": "Навык \"{{name}}\" не найден", - "cannot_modify_builtin": "Встроенные навыки нельзя создавать или удалять", - "cannot_open_builtin": "Встроенные навыки нельзя открыть как файлы" + "skill_not_found": "Навык \"{{name}}\" не найден" } } diff --git a/src/i18n/locales/tr/skills.json b/src/i18n/locales/tr/skills.json index 235b9d55fc5..459d9c8f6db 100644 --- a/src/i18n/locales/tr/skills.json +++ b/src/i18n/locales/tr/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Gerekli alanlar eksik: skillName veya source", "manager_unavailable": "Beceri yöneticisi kullanılamıyor", "missing_delete_fields": "Gerekli alanlar eksik: skillName veya source", - "skill_not_found": "\"{{name}}\" becerisi bulunamadı", - "cannot_modify_builtin": "Yerleşik beceriler oluşturulamaz veya silinemez", - "cannot_open_builtin": "Yerleşik beceriler dosya olarak açılamaz" + "skill_not_found": "\"{{name}}\" becerisi bulunamadı" } } diff --git a/src/i18n/locales/vi/skills.json b/src/i18n/locales/vi/skills.json index 47e433ee579..3bd28a8c0b8 100644 --- a/src/i18n/locales/vi/skills.json +++ b/src/i18n/locales/vi/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "Thiếu các trường bắt buộc: skillName hoặc source", "manager_unavailable": "Trình quản lý kỹ năng không khả dụng", "missing_delete_fields": "Thiếu các trường bắt buộc: skillName hoặc source", - "skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"", - "cannot_modify_builtin": "Không thể tạo hoặc xóa kỹ năng tích hợp sẵn", - "cannot_open_builtin": "Không thể mở kỹ năng tích hợp sẵn dưới dạng tệp" + "skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"" } } diff --git a/src/i18n/locales/zh-CN/skills.json b/src/i18n/locales/zh-CN/skills.json index 719bc722a5d..ade7833363e 100644 --- a/src/i18n/locales/zh-CN/skills.json +++ b/src/i18n/locales/zh-CN/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "缺少必填字段:skillName 或 source", "manager_unavailable": "技能管理器不可用", "missing_delete_fields": "缺少必填字段:skillName 或 source", - "skill_not_found": "未找到技能 \"{{name}}\"", - "cannot_modify_builtin": "内置技能无法创建或删除", - "cannot_open_builtin": "内置技能无法作为文件打开" + "skill_not_found": "未找到技能 \"{{name}}\"" } } diff --git a/src/i18n/locales/zh-TW/skills.json b/src/i18n/locales/zh-TW/skills.json index 2d9a52be1e7..e2c1fcf305c 100644 --- a/src/i18n/locales/zh-TW/skills.json +++ b/src/i18n/locales/zh-TW/skills.json @@ -11,8 +11,6 @@ "missing_update_modes_fields": "缺少必填欄位:skillName 或 source", "manager_unavailable": "技能管理器無法使用", "missing_delete_fields": "缺少必填欄位:skillName 或 source", - "skill_not_found": "找不到技能「{{name}}」", - "cannot_modify_builtin": "內建技能無法建立或刪除", - "cannot_open_builtin": "內建技能無法作為檔案開啟" + "skill_not_found": "找不到技能「{{name}}」" } } diff --git a/src/package.json b/src/package.json index 27c07f8c8a8..c16e36ae0c0 100644 --- a/src/package.json +++ b/src/package.json @@ -439,8 +439,6 @@ "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 f6f86e85736..c527fddb88c 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -15,7 +15,6 @@ import { 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 } @@ -179,19 +178,13 @@ export class SkillsManager { /** * Get skills available for the current mode. - * Resolves overrides: project > global > built-in, mode-specific > generic. + * Resolves overrides: project > global, 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()) { // Check if skill is available in current mode: // - modeSlugs undefined or empty = available in all modes ("Any mode") @@ -232,14 +225,13 @@ export class SkillsManager { /** * Determine if newSkill should override existingSkill based on priority rules. - * Priority: project > global > built-in, mode-specific > generic + * Priority: project > global, mode-specific > generic */ private shouldOverrideSkill(existing: SkillMetadata, newSkill: SkillMetadata): boolean { - // Define source priority: project > global > built-in + // Define source priority: project > global const sourcePriority: Record = { - project: 3, - global: 2, - "built-in": 1, + project: 2, + global: 1, } const existingPriority = sourcePriority[existing.source] ?? 0 @@ -275,21 +267,13 @@ export class SkillsManager { const modeSkills = this.getSkillsForMode(currentMode) skill = modeSkills.find((s) => s.name === name) } else { - // Fall back to any skill with this name (check discovered skills first, then built-in) + // Fall back to any skill with this name 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 + // Read skill content from disk const fileContent = await fs.readFile(skill.path, "utf-8") const { content: body } = matter(fileContent) @@ -599,7 +583,7 @@ Add your skill instructions here. const modesList = await this.getAvailableModes() // Priority rules for skills with the same name: - // 1. Source level: project > global > built-in (handled by shouldOverrideSkill in getSkillsForMode) + // 1. Source level: project > global (handled by shouldOverrideSkill in getSkillsForMode) // 2. Within the same source level: later-processed directories override earlier ones // (via Map.set replacement during discovery - same source+mode+name key gets replaced) // diff --git a/src/services/skills/__tests__/SkillsManager.spec.ts b/src/services/skills/__tests__/SkillsManager.spec.ts index b0fee079bb8..d36582d8939 100644 --- a/src/services/skills/__tests__/SkillsManager.spec.ts +++ b/src/services/skills/__tests__/SkillsManager.spec.ts @@ -109,14 +109,6 @@ vi.mock("../../../i18n", () => ({ }, })) -// 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" import { ClineProvider } from "../../../core/webview/ClineProvider" diff --git a/src/services/skills/__tests__/generate-built-in-skills.spec.ts b/src/services/skills/__tests__/generate-built-in-skills.spec.ts deleted file mode 100644 index 10b44b87163..00000000000 --- a/src/services/skills/__tests__/generate-built-in-skills.spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * 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/services/skills/built-in-skills.ts b/src/services/skills/built-in-skills.ts deleted file mode 100644 index 2a2ca38d391..00000000000 --- a/src/services/skills/built-in-skills.ts +++ /dev/null @@ -1,421 +0,0 @@ -/** - * 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 - */ - -import { SkillMetadata, SkillContent } from "../../shared/skills" - -interface BuiltInSkillDefinition { - name: string - description: string - instructions: string -} - -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 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.`, - }, - "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 deleted file mode 100644 index be52e91c890..00000000000 --- a/src/services/skills/built-in/create-mcp-server/SKILL.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -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 deleted file mode 100644 index ec43ac9bea1..00000000000 --- a/src/services/skills/built-in/create-mode/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -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 deleted file mode 100644 index 517040c010c..00000000000 --- a/src/services/skills/generate-built-in-skills.ts +++ /dev/null @@ -1,300 +0,0 @@ -#!/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 - */ - -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 cbcc71d7b78..f5151181f6d 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 (or "" for built-in skills) - source: "global" | "project" | "built-in" // Where the skill was discovered + path: string // Absolute path to SKILL.md + source: "global" | "project" // Where the skill was discovered /** * @deprecated Use modeSlugs instead. Kept for backward compatibility. * If set, skill is only available in this mode. diff --git a/webview-ui/src/components/settings/SkillItem.tsx b/webview-ui/src/components/settings/SkillItem.tsx index cd11f4553d4..7c2bc213203 100644 --- a/webview-ui/src/components/settings/SkillItem.tsx +++ b/webview-ui/src/components/settings/SkillItem.tsx @@ -53,9 +53,6 @@ export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) [skill.name, skill.source, skill.mode], ) - // Built-in skills cannot change mode - const isBuiltIn = skill.source === "built-in" - return (
{/* Skill name and description */} @@ -70,27 +67,21 @@ export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) {/* Mode dropdown */}
- {isBuiltIn ? ( - - {skill.mode || t("settings:skills.modeAny")} - - ) : ( - - - - )} + + +
{/* Action buttons */} @@ -106,18 +97,16 @@ export const SkillItem: React.FC = ({ skill, onEdit, onDelete }) - {!isBuiltIn && ( - - - - )} + + +
) diff --git a/webview-ui/src/components/settings/SkillsSettings.tsx b/webview-ui/src/components/settings/SkillsSettings.tsx index cdcee78083b..bf81d0c6c70 100644 --- a/webview-ui/src/components/settings/SkillsSettings.tsx +++ b/webview-ui/src/components/settings/SkillsSettings.tsx @@ -164,8 +164,6 @@ export const SkillsSettings: React.FC = () => { // Render a single skill item const renderSkillItem = useCallback( (skill: SkillMetadata) => { - const isBuiltIn = skill.source === "built-in" - return (
{ {/* Actions */}
- {/* Mode settings button (gear icon) - only for non-built-in skills */} - {!isBuiltIn && ( - - - - )} + {/* Mode settings button (gear icon) */} + + + - {!isBuiltIn && ( - - - - )} + + +
diff --git a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx index a8406e62944..2c3f1ec3b2c 100644 --- a/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SkillItem.spec.tsx @@ -92,13 +92,6 @@ const mockSkillWithMode: SkillMetadata = { mode: "architect", } -const mockBuiltInSkill: SkillMetadata = { - name: "built-in-skill", - description: "A built-in skill", - path: "", - source: "built-in", -} - describe("SkillItem", () => { const mockOnEdit = vi.fn() const mockOnDelete = vi.fn() @@ -119,7 +112,7 @@ describe("SkillItem", () => { expect(screen.getByText("A test skill description")).toBeInTheDocument() }) - it("renders mode dropdown for non-built-in skills", () => { + it("renders mode dropdown", () => { render() expect(screen.getByTestId("select")).toBeInTheDocument() @@ -139,15 +132,6 @@ describe("SkillItem", () => { expect(select).toHaveAttribute("data-value", "__any__") }) - it("does not render mode dropdown for built-in skills", () => { - render() - - // Should not have a select element - expect(screen.queryByTestId("select")).not.toBeInTheDocument() - // Should have a static badge instead - expect(screen.getByText("Any mode")).toBeInTheDocument() - }) - it("calls onEdit when edit button is clicked", () => { render() @@ -158,24 +142,16 @@ describe("SkillItem", () => { expect(mockOnEdit).toHaveBeenCalledTimes(1) }) - it("calls onDelete when delete button is clicked for non-built-in skills", () => { + it("calls onDelete when delete button is clicked", () => { render() const buttons = screen.getAllByTestId("button") - // Find the delete button (second one for non-built-in) + // Find the delete button (second one) fireEvent.click(buttons[1]) expect(mockOnDelete).toHaveBeenCalledTimes(1) }) - it("does not render delete button for built-in skills", () => { - render() - - // Should only have 1 button (edit) for built-in skills - const buttons = screen.getAllByTestId("button") - expect(buttons).toHaveLength(1) - }) - it("calls onEdit when clicking on skill name area", () => { render() @@ -239,7 +215,7 @@ describe("SkillItem", () => { expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground") }) - it("renders edit and delete buttons for non-built-in skills", () => { + it("renders edit and delete buttons", () => { render() const buttons = screen.getAllByTestId("button") diff --git a/webview-ui/src/components/ui/hooks/useRouterModels.ts b/webview-ui/src/components/ui/hooks/useRouterModels.ts index 27e888b7b57..58dda02bdd2 100644 --- a/webview-ui/src/components/ui/hooks/useRouterModels.ts +++ b/webview-ui/src/components/ui/hooks/useRouterModels.ts @@ -12,7 +12,9 @@ type UseRouterModelsOptions = { const getRouterModels = async (provider?: string) => new Promise((resolve, reject) => { const cleanup = () => { - window.removeEventListener("message", handler) + if (typeof window !== "undefined") { + window.removeEventListener("message", handler) + } } const timeout = setTimeout(() => {