diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 14e33596d1e..4f278db58a4 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 494fbae4fc1..b6a9dd4d0dc 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 63c10ff790b..6621cf9524f 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 6cd97472efd..0fba7640805 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 92dc790e1f9..1c60189b2f8 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 37a40d8742c..114e129f45a 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 0972b8a77e1..62ab4dcb6e6 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 d6b6f1faf3f..aae4d5d7b10 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 935bc3b5f66..fae7c42be93 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 71ed70fce86..eb2db5ac84e 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 bcb5754f9b9..a7ef4b075a7 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 2fb6a76b4cc..6e7e0b8a3eb 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 e3ee789b047..1d0a5f3c4a7 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 8cf11bf4351..bb7d3c0f233 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 d1872ce628b..953f52ea790 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 fd16391491f..21aca727a18 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 05ad66aedcb..30913e16e9a 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 66d7666d4ad..6892c7c8f15 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 59e1e3d14b4..f88120098d4 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 97f7c74bb34..e81b7d589a6 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 2b15dd40b94..1c800d4d372 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 61b8059e104..954082185ac 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 00000000000..e40f351003f
--- /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: path.join("/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: path.join("/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: path.join("/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: path.join("/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(path.join(".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 a78b2187f82..00549675c03 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 a592c3e885a..f7113d5df26 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 fb71e7e2a91..f0a0b355cf6 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) {