From 228727e2e666defbb461ac4122178ce231670d3f Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 28 Jul 2025 16:32:55 -0400 Subject: [PATCH 1/3] Add support for slash command frontmatter descriptions --- pnpm-lock.yaml | 43 ++++ src/core/mentions/index.ts | 7 +- src/core/webview/webviewMessageHandler.ts | 2 + src/i18n/locales/ca/common.json | 2 +- src/i18n/locales/de/common.json | 2 +- src/i18n/locales/en/common.json | 2 +- src/i18n/locales/es/common.json | 2 +- src/i18n/locales/fr/common.json | 2 +- src/i18n/locales/hi/common.json | 2 +- src/i18n/locales/id/common.json | 2 +- src/i18n/locales/it/common.json | 2 +- src/i18n/locales/ja/common.json | 2 +- src/i18n/locales/ko/common.json | 2 +- src/i18n/locales/nl/common.json | 2 +- src/i18n/locales/pl/common.json | 2 +- src/i18n/locales/pt-BR/common.json | 2 +- src/i18n/locales/ru/common.json | 2 +- src/i18n/locales/tr/common.json | 2 +- src/i18n/locales/vi/common.json | 2 +- src/i18n/locales/zh-CN/common.json | 2 +- src/i18n/locales/zh-TW/common.json | 2 +- src/package.json | 1 + .../__tests__/frontmatter-commands.spec.ts | 231 ++++++++++++++++++ src/services/command/commands.ts | 45 +++- src/shared/ExtensionMessage.ts | 1 + webview-ui/src/utils/context-mentions.ts | 2 + 26 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 src/services/command/__tests__/frontmatter-commands.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14e33596d1..4f278db58a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -654,6 +654,9 @@ importers: google-auth-library: specifier: ^9.15.1 version: 9.15.1 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.8.3) @@ -5644,6 +5647,10 @@ packages: exsolve@1.0.5: resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -6026,6 +6033,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -6340,6 +6351,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -8432,6 +8447,10 @@ packages: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} @@ -8744,6 +8763,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -15174,6 +15197,10 @@ snapshots: exsolve@1.0.5: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -15597,6 +15624,13 @@ snapshots: graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -15959,6 +15993,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -18479,6 +18515,11 @@ snapshots: screenfull@5.2.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + seed-random@2.2.0: {} semver@5.7.2: {} @@ -18873,6 +18914,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-bom@5.0.0: {} diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 494fbae4fc..b6a9dd4d0d 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -218,7 +218,12 @@ export async function parseMentions( try { const command = await getCommand(cwd, commandName) if (command) { - parsedText += `\n\n\n${command.content}\n` + let commandOutput = "" + if (command.description) { + commandOutput += `Description: ${command.description}\n\n` + } + commandOutput += command.content + parsedText += `\n\n\n${commandOutput}\n` } else { parsedText += `\n\n\nCommand '${commandName}' not found. Available commands can be found in .roo/commands/ or ~/.roo/commands/\n` } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 63c10ff790..6621cf9524 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2366,6 +2366,7 @@ export const webviewMessageHandler = async ( name: command.name, source: command.source, filePath: command.filePath, + description: command.description, })) await provider.postMessageToWebview({ @@ -2524,6 +2525,7 @@ export const webviewMessageHandler = async ( name: command.name, source: command.source, filePath: command.filePath, + description: command.description, })) await provider.postMessageToWebview({ type: "commands", diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 6cd97472ef..0fba764080 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -80,7 +80,7 @@ "no_workspace_for_project_command": "No s'ha trobat cap carpeta d'espai de treball per a l'ordre del projecte", "command_already_exists": "L'ordre \"{{commandName}}\" ja existeix", "create_command_failed": "Error en crear l'ordre", - "command_template_content": "Aquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.", + "command_template_content": "---\ndescription: \"Breu descripció del que fa aquesta ordre\"\n---\n\nAquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.", "claudeCode": { "processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.", "errorOutput": "Sortida d'error: {{output}}", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 92dc790e1f..1c60189b2f 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Kein Arbeitsbereich-Ordner für Projektbefehl gefunden", "command_already_exists": "Befehl \"{{commandName}}\" existiert bereits", "create_command_failed": "Fehler beim Erstellen des Befehls", - "command_template_content": "Dies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.", + "command_template_content": "---\ndescription: \"Kurze Beschreibung dessen, was dieser Befehl macht\"\n---\n\nDies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.", "claudeCode": { "processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.", "errorOutput": "Fehlerausgabe: {{output}}", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 37a40d8742..114e129f45 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "No workspace folder found for project command", "command_already_exists": "Command \"{{commandName}}\" already exists", "create_command_failed": "Failed to create command", - "command_template_content": "This is a new slash command. Edit this file to customize the command behavior.", + "command_template_content": "---\ndescription: \"Brief description of what this command does\"\n---\n\nThis is a new slash command. Edit this file to customize the command behavior.", "claudeCode": { "processExited": "Claude Code process exited with code {{exitCode}}.", "errorOutput": "Error output: {{output}}", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 0972b8a77e..62ab4dcb6e 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "No se encontró carpeta de espacio de trabajo para comando de proyecto", "command_already_exists": "El comando \"{{commandName}}\" ya existe", "create_command_failed": "Error al crear comando", - "command_template_content": "Este es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.", + "command_template_content": "---\ndescription: \"Breve descripción de lo que hace este comando\"\n---\n\nEste es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.", "claudeCode": { "processExited": "El proceso de Claude Code terminó con código {{exitCode}}.", "errorOutput": "Salida de error: {{output}}", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index d6b6f1faf3..aae4d5d7b1 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Aucun dossier d'espace de travail trouvé pour la commande de projet", "command_already_exists": "La commande \"{{commandName}}\" existe déjà", "create_command_failed": "Échec de la création de la commande", - "command_template_content": "Ceci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.", + "command_template_content": "---\ndescription: \"Brève description de ce que fait cette commande\"\n---\n\nCeci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.", "claudeCode": { "processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.", "errorOutput": "Sortie d'erreur : {{output}}", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 935bc3b5f6..fae7c42be9 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "प्रोजेक्ट कमांड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला", "command_already_exists": "कमांड \"{{commandName}}\" पहले से मौजूद है", "create_command_failed": "कमांड बनाने में विफल", - "command_template_content": "यह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।", + "command_template_content": "---\ndescription: \"इस कमांड के कार्य का संक्षिप्त विवरण\"\n---\n\nयह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।", "claudeCode": { "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", "errorOutput": "त्रुटि आउटपुट: {{output}}", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 71ed70fce8..eb2db5ac84 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Tidak ditemukan folder workspace untuk perintah proyek", "command_already_exists": "Perintah \"{{commandName}}\" sudah ada", "create_command_failed": "Gagal membuat perintah", - "command_template_content": "Ini adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.", + "command_template_content": "---\ndescription: \"Deskripsi singkat tentang fungsi perintah ini\"\n---\n\nIni adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.", "claudeCode": { "processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.", "errorOutput": "Output error: {{output}}", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index bcb5754f9b..a7ef4b075a 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Nessuna cartella workspace trovata per il comando di progetto", "command_already_exists": "Il comando \"{{commandName}}\" esiste già", "create_command_failed": "Errore nella creazione del comando", - "command_template_content": "Questo è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.", + "command_template_content": "---\ndescription: \"Breve descrizione di cosa fa questo comando\"\n---\n\nQuesto è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.", "claudeCode": { "processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.", "errorOutput": "Output di errore: {{output}}", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 2fb6a76b4c..6e7e0b8a3e 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "プロジェクトコマンド用のワークスペースフォルダが見つかりません", "command_already_exists": "コマンド \"{{commandName}}\" は既に存在します", "create_command_failed": "コマンドの作成に失敗しました", - "command_template_content": "これは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。", + "command_template_content": "---\ndescription: \"このコマンドが何をするかの簡潔な説明\"\n---\n\nこれは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。", "claudeCode": { "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", "errorOutput": "エラー出力:{{output}}", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index e3ee789b04..1d0a5f3c4a 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "프로젝트 명령용 워크스페이스 폴더를 찾을 수 없습니다", "command_already_exists": "명령 \"{{commandName}}\"이(가) 이미 존재합니다", "create_command_failed": "명령 생성에 실패했습니다", - "command_template_content": "이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.", + "command_template_content": "---\ndescription: \"이 명령이 수행하는 작업에 대한 간단한 설명\"\n---\n\n이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.", "claudeCode": { "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", "errorOutput": "오류 출력: {{output}}", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 8cf11bf435..bb7d3c0f23 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Geen werkruimtemap gevonden voor projectopdracht", "command_already_exists": "Opdracht \"{{commandName}}\" bestaat al", "create_command_failed": "Kan opdracht niet aanmaken", - "command_template_content": "Dit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.", + "command_template_content": "---\ndescription: \"Korte beschrijving van wat deze opdracht doet\"\n---\n\nDit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.", "claudeCode": { "processExited": "Claude Code proces beëindigd met code {{exitCode}}.", "errorOutput": "Foutuitvoer: {{output}}", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index d1872ce628..953f52ea79 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Nie znaleziono folderu obszaru roboczego dla polecenia projektu", "command_already_exists": "Polecenie \"{{commandName}}\" już istnieje", "create_command_failed": "Nie udało się utworzyć polecenia", - "command_template_content": "To jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.", + "command_template_content": "---\ndescription: \"Krótki opis tego, co robi to polecenie\"\n---\n\nTo jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.", "claudeCode": { "processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.", "errorOutput": "Wyjście błędu: {{output}}", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index fd16391491..21aca727a1 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -81,7 +81,7 @@ "no_workspace_for_project_command": "Nenhuma pasta de workspace encontrada para comando de projeto", "command_already_exists": "Comando \"{{commandName}}\" já existe", "create_command_failed": "Falha ao criar comando", - "command_template_content": "Este é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.", + "command_template_content": "---\ndescription: \"Breve descrição do que este comando faz\"\n---\n\nEste é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.", "claudeCode": { "processExited": "O processo Claude Code saiu com código {{exitCode}}.", "errorOutput": "Saída de erro: {{output}}", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 05ad66aedc..30913e16e9 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Не найдена папка рабочего пространства для команды проекта", "command_already_exists": "Команда \"{{commandName}}\" уже существует", "create_command_failed": "Не удалось создать команду", - "command_template_content": "Это новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.", + "command_template_content": "---\ndescription: \"Краткое описание того, что делает эта команда\"\n---\n\nЭто новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.", "claudeCode": { "processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.", "errorOutput": "Вывод ошибки: {{output}}", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 66d7666d4a..6892c7c8f1 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Proje komutu için çalışma alanı klasörü bulunamadı", "command_already_exists": "\"{{commandName}}\" komutu zaten mevcut", "create_command_failed": "Komut oluşturulamadı", - "command_template_content": "Bu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.", + "command_template_content": "---\ndescription: \"Bu komutun ne yaptığının kısa açıklaması\"\n---\n\nBu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.", "claudeCode": { "processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.", "errorOutput": "Hata çıktısı: {{output}}", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 59e1e3d14b..f88120098d 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -77,7 +77,7 @@ "no_workspace_for_project_command": "Không tìm thấy thư mục workspace cho lệnh dự án", "command_already_exists": "Lệnh \"{{commandName}}\" đã tồn tại", "create_command_failed": "Không thể tạo lệnh", - "command_template_content": "Đây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.", + "command_template_content": "---\ndescription: \"Mô tả ngắn gọn về chức năng của lệnh này\"\n---\n\nĐây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.", "claudeCode": { "processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.", "errorOutput": "Đầu ra lỗi: {{output}}", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 97f7c74bb3..e81b7d589a 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -82,7 +82,7 @@ "no_workspace_for_project_command": "未找到项目命令的工作区文件夹", "command_already_exists": "命令 \"{{commandName}}\" 已存在", "create_command_failed": "创建命令失败", - "command_template_content": "这是一个新的斜杠命令。编辑此文件以自定义命令行为。", + "command_template_content": "---\ndescription: \"此命令功能的简要描述\"\n---\n\n这是一个新的斜杠命令。编辑此文件以自定义命令行为。", "claudeCode": { "processExited": "Claude Code 进程退出,退出码:{{exitCode}}。", "errorOutput": "错误输出:{{output}}", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 2b15dd40b9..1c800d4d37 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -76,7 +76,7 @@ "no_workspace_for_project_command": "找不到專案指令的工作區資料夾", "command_already_exists": "指令 \"{{commandName}}\" 已存在", "create_command_failed": "建立指令失敗", - "command_template_content": "這是一個新的斜線指令。編輯此檔案以自訂指令行為。", + "command_template_content": "---\ndescription: \"此指令功能的簡要描述\"\n---\n\n這是一個新的斜線指令。編輯此檔案以自訂指令行為。", "claudeCode": { "processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。", "errorOutput": "錯誤輸出:{{output}}", diff --git a/src/package.json b/src/package.json index 61b8059e10..954082185a 100644 --- a/src/package.json +++ b/src/package.json @@ -442,6 +442,7 @@ "fzf": "^0.5.2", "get-folder-size": "^5.0.0", "google-auth-library": "^9.15.1", + "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", "isbinaryfile": "^5.0.2", diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts new file mode 100644 index 0000000000..749480ce0f --- /dev/null +++ b/src/services/command/__tests__/frontmatter-commands.spec.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import fs from "fs/promises" +import * as path from "path" +import { getCommand, getCommands } from "../commands" + +// Mock fs and path modules +vi.mock("fs/promises") +vi.mock("../roo-config", () => ({ + getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"), + getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"), +})) + +const mockFs = vi.mocked(fs) + +describe("Command loading with frontmatter", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("getCommand with frontmatter", () => { + it("should load command with description from frontmatter", async () => { + const commandContent = `--- +description: Sets up the development environment +author: John Doe +--- + +# Setup Command + +Run the following commands: +\`\`\`bash +npm install +npm run build +\`\`\`` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", + source: "project", + filePath: "/test/cwd/.roo/commands/setup.md", + description: "Sets up the development environment", + }) + }) + + it("should load command without frontmatter", async () => { + const commandContent = `# Setup Command + +Run the following commands: +\`\`\`bash +npm install +npm run build +\`\`\`` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", + source: "project", + filePath: "/test/cwd/.roo/commands/setup.md", + description: undefined, + }) + }) + + it("should handle empty description in frontmatter", async () => { + const commandContent = `--- +description: "" +author: John Doe +--- + +# Setup Command + +Command content here.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result?.description).toBeUndefined() + }) + + it("should handle malformed frontmatter gracefully", async () => { + const commandContent = `--- +description: Test +invalid: yaml: [ +--- + +# Setup Command + +Command content here.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: commandContent.trim(), + source: "project", + filePath: "/test/cwd/.roo/commands/setup.md", + description: undefined, + }) + }) + + it("should prioritize project commands over global commands", async () => { + const projectCommandContent = `--- +description: Project-specific setup +--- + +# Project Setup + +Project-specific setup instructions.` + + const globalCommandContent = `--- +description: Global setup +--- + +# Global Setup + +Global setup instructions.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi + .fn() + .mockResolvedValueOnce(projectCommandContent) // First call for project + .mockResolvedValueOnce(globalCommandContent) // Second call for global (shouldn't be used) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Project Setup\n\nProject-specific setup instructions.", + source: "project", + filePath: "/test/cwd/.roo/commands/setup.md", + description: "Project-specific setup", + }) + }) + + it("should fall back to global command if project command doesn't exist", async () => { + const globalCommandContent = `--- +description: Global setup command +--- + +# Global Setup + +Global setup instructions.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi + .fn() + .mockRejectedValueOnce(new Error("File not found")) // Project command doesn't exist + .mockResolvedValueOnce(globalCommandContent) // Global command exists + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Global Setup\n\nGlobal setup instructions.", + source: "global", + filePath: expect.stringContaining("/.roo/commands/setup.md"), + description: "Global setup command", + }) + }) + }) + + describe("getCommands with frontmatter", () => { + it("should load multiple commands with descriptions", async () => { + const setupContent = `--- +description: Sets up the development environment +--- + +# Setup Command + +Setup instructions.` + + const deployContent = `--- +description: Deploys the application to production +--- + +# Deploy Command + +Deploy instructions.` + + const buildContent = `# Build Command + +Build instructions without frontmatter.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readdir = vi.fn().mockResolvedValue([ + { name: "setup.md", isFile: () => true }, + { name: "deploy.md", isFile: () => true }, + { name: "build.md", isFile: () => true }, + { name: "not-markdown.txt", isFile: () => true }, // Should be ignored + ]) + mockFs.readFile = vi + .fn() + .mockResolvedValueOnce(setupContent) + .mockResolvedValueOnce(deployContent) + .mockResolvedValueOnce(buildContent) + + const result = await getCommands("/test/cwd") + + expect(result).toHaveLength(3) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "setup", + description: "Sets up the development environment", + }), + expect.objectContaining({ + name: "deploy", + description: "Deploys the application to production", + }), + expect.objectContaining({ + name: "build", + description: undefined, + }), + ]), + ) + }) + }) +}) diff --git a/src/services/command/commands.ts b/src/services/command/commands.ts index a78b2187f8..00549675c0 100644 --- a/src/services/command/commands.ts +++ b/src/services/command/commands.ts @@ -1,5 +1,6 @@ import fs from "fs/promises" import * as path from "path" +import matter from "gray-matter" import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config" export interface Command { @@ -7,6 +8,7 @@ export interface Command { content: string source: "global" | "project" filePath: string + description?: string } /** @@ -65,11 +67,31 @@ async function tryLoadCommand( try { const content = await fs.readFile(filePath, "utf-8") + + let parsed + let description: string | undefined + let commandContent: string + + try { + // Try to parse frontmatter with gray-matter + parsed = matter(content) + description = + typeof parsed.data.description === "string" && parsed.data.description.trim() + ? parsed.data.description.trim() + : undefined + commandContent = parsed.content.trim() + } catch (frontmatterError) { + // If frontmatter parsing fails, treat the entire content as command content + description = undefined + commandContent = content.trim() + } + return { name, - content: content.trim(), + content: commandContent, source, filePath, + description, } } catch (error) { // File doesn't exist or can't be read @@ -113,13 +135,32 @@ async function scanCommandDirectory( try { const content = await fs.readFile(filePath, "utf-8") + let parsed + let description: string | undefined + let commandContent: string + + try { + // Try to parse frontmatter with gray-matter + parsed = matter(content) + description = + typeof parsed.data.description === "string" && parsed.data.description.trim() + ? parsed.data.description.trim() + : undefined + commandContent = parsed.content.trim() + } catch (frontmatterError) { + // If frontmatter parsing fails, treat the entire content as command content + description = undefined + commandContent = content.trim() + } + // Project commands override global ones if (source === "project" || !commands.has(commandName)) { commands.set(commandName, { name: commandName, - content: content.trim(), + content: commandContent, source, filePath, + description, }) } } catch (error) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index a592c3e885..f7113d5df2 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -24,6 +24,7 @@ export interface Command { name: string source: "global" | "project" filePath?: string + description?: string } // Type for marketplace installed metadata diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index fb71e7e2a9..f0a0b355cf 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -153,11 +153,13 @@ export function getContextMenuOptions( type: ContextMenuOptionType.Command, value: result.item.original.name, slashCommand: `/${result.item.original.name}`, + description: result.item.original.description, })) : commands.map((command) => ({ type: ContextMenuOptionType.Command, value: command.name, slashCommand: `/${command.name}`, + description: command.description, })) if (matchingCommands.length > 0) { From 3ca40c2b422e314719cb47470ac4556b2400e259 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 28 Jul 2025 16:46:14 -0400 Subject: [PATCH 2/3] Fix windows tests --- .../command/__tests__/frontmatter-commands.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts index 749480ce0f..c8e6924614 100644 --- a/src/services/command/__tests__/frontmatter-commands.spec.ts +++ b/src/services/command/__tests__/frontmatter-commands.spec.ts @@ -41,7 +41,7 @@ npm run build name: "setup", content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", source: "project", - filePath: "/test/cwd/.roo/commands/setup.md", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: "Sets up the development environment", }) }) @@ -64,7 +64,7 @@ npm run build name: "setup", content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", source: "project", - filePath: "/test/cwd/.roo/commands/setup.md", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: undefined, }) }) @@ -106,7 +106,7 @@ Command content here.` name: "setup", content: commandContent.trim(), source: "project", - filePath: "/test/cwd/.roo/commands/setup.md", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: undefined, }) }) @@ -140,7 +140,7 @@ Global setup instructions.` name: "setup", content: "# Project Setup\n\nProject-specific setup instructions.", source: "project", - filePath: "/test/cwd/.roo/commands/setup.md", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), description: "Project-specific setup", }) }) From 53d3ac41b1796543222b916ed7f3ade6932c5b85 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 28 Jul 2025 16:53:40 -0400 Subject: [PATCH 3/3] Another fix --- src/services/command/__tests__/frontmatter-commands.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts index c8e6924614..e40f351003 100644 --- a/src/services/command/__tests__/frontmatter-commands.spec.ts +++ b/src/services/command/__tests__/frontmatter-commands.spec.ts @@ -166,7 +166,7 @@ Global setup instructions.` name: "setup", content: "# Global Setup\n\nGlobal setup instructions.", source: "global", - filePath: expect.stringContaining("/.roo/commands/setup.md"), + filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")), description: "Global setup command", }) })