Просмотр исходного кода

Add support for slash command frontmatter descriptions (#6314)

Matt Rubens 5 месяцев назад
Родитель
Сommit
182679d34c

+ 43 - 0
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([email protected])
@@ -5644,6 +5647,10 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
+    engines: {node: '>=0.10.0'}
+
   [email protected]:
     resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
 
@@ -6026,6 +6033,10 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
 
+  [email protected]:
+    resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
+    engines: {node: '>=6.0'}
+
   [email protected]:
     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
 
+  [email protected]:
+    resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
+    engines: {node: '>=0.10.0'}
+
   [email protected]:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
@@ -8432,6 +8447,10 @@ packages:
     resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
     engines: {node: '>=0.10.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
+    engines: {node: '>=4'}
+
   [email protected]:
     resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==}
 
@@ -8744,6 +8763,10 @@ packages:
     resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
     engines: {node: '>=12'}
 
+  [email protected]:
+    resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
+    engines: {node: '>=0.10.0'}
+
   [email protected]:
     resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
     engines: {node: '>=4'}
@@ -15174,6 +15197,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      is-extendable: 0.1.1
+
   [email protected]: {}
 
   [email protected]: {}
@@ -15597,6 +15624,13 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      js-yaml: 3.14.1
+      kind-of: 6.0.3
+      section-matter: 1.0.0
+      strip-bom-string: 1.0.0
+
   [email protected]:
     dependencies:
       gaxios: 6.7.1
@@ -15959,6 +15993,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -18479,6 +18515,11 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      extend-shallow: 2.0.1
+      kind-of: 6.0.3
+
   [email protected]: {}
 
   [email protected]: {}
@@ -18873,6 +18914,8 @@ snapshots:
     dependencies:
       ansi-regex: 6.1.0
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}

+ 6 - 1
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<command name="${commandName}">\n${command.content}\n</command>`
+				let commandOutput = ""
+				if (command.description) {
+					commandOutput += `Description: ${command.description}\n\n`
+				}
+				commandOutput += command.content
+				parsedText += `\n\n<command name="${commandName}">\n${commandOutput}\n</command>`
 			} else {
 				parsedText += `\n\n<command name="${commandName}">\nCommand '${commandName}' not found. Available commands can be found in .roo/commands/ or ~/.roo/commands/\n</command>`
 			}

+ 2 - 0
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",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 1
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}}",

+ 1 - 0
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",

+ 231 - 0
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,
+					}),
+				]),
+			)
+		})
+	})
+})

+ 43 - 2
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) {

+ 1 - 0
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

+ 2 - 0
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) {