diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index da73c569201..63c10ff790b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2365,6 +2365,7 @@ export const webviewMessageHandler = async ( const commandList = commands.map((command) => ({ name: command.name, source: command.source, + filePath: command.filePath, })) await provider.postMessageToWebview({ @@ -2381,5 +2382,170 @@ export const webviewMessageHandler = async ( } break } + case "openCommandFile": { + try { + if (message.text) { + const { getCommand } = await import("../../services/command/commands") + const command = await getCommand(provider.cwd || "", message.text) + + if (command && command.filePath) { + openFile(command.filePath) + } else { + vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) + } + } + } catch (error) { + provider.log( + `Error opening command file: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.open_command_file")) + } + break + } + case "deleteCommand": { + try { + if (message.text && message.values?.source) { + const { getCommand } = await import("../../services/command/commands") + const command = await getCommand(provider.cwd || "", message.text) + + if (command && command.filePath) { + // Delete the command file + await fs.unlink(command.filePath) + provider.log(`Deleted command file: ${command.filePath}`) + } else { + vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) + } + } + } catch (error) { + provider.log(`Error deleting command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + vscode.window.showErrorMessage(t("common:errors.delete_command")) + } + break + } + case "createCommand": { + try { + const source = message.values?.source as "global" | "project" + const fileName = message.text // Custom filename from user input + + if (!source) { + provider.log("Missing source for createCommand") + break + } + + // Determine the commands directory based on source + let commandsDir: string + if (source === "global") { + const globalConfigDir = path.join(os.homedir(), ".roo") + commandsDir = path.join(globalConfigDir, "commands") + } else { + // Project commands + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command")) + break + } + commandsDir = path.join(workspaceRoot, ".roo", "commands") + } + + // Ensure the commands directory exists + await fs.mkdir(commandsDir, { recursive: true }) + + // Use provided filename or generate a unique one + let commandName: string + if (fileName && fileName.trim()) { + let cleanFileName = fileName.trim() + + // Strip leading slash if present + if (cleanFileName.startsWith("/")) { + cleanFileName = cleanFileName.substring(1) + } + + // Remove .md extension if present BEFORE slugification + if (cleanFileName.toLowerCase().endsWith(".md")) { + cleanFileName = cleanFileName.slice(0, -3) + } + + // Slugify the command name: lowercase, replace spaces with dashes, remove special characters + commandName = cleanFileName + .toLowerCase() + .replace(/\s+/g, "-") // Replace spaces with dashes + .replace(/[^a-z0-9-]/g, "") // Remove special characters except dashes + .replace(/-+/g, "-") // Replace multiple dashes with single dash + .replace(/^-|-$/g, "") // Remove leading/trailing dashes + + // Ensure we have a valid command name + if (!commandName || commandName.length === 0) { + commandName = "new-command" + } + } else { + // Generate a unique command name + commandName = "new-command" + let counter = 1 + let filePath = path.join(commandsDir, `${commandName}.md`) + + while ( + await fs + .access(filePath) + .then(() => true) + .catch(() => false) + ) { + commandName = `new-command-${counter}` + filePath = path.join(commandsDir, `${commandName}.md`) + counter++ + } + } + + const filePath = path.join(commandsDir, `${commandName}.md`) + + // Check if file already exists + if ( + await fs + .access(filePath) + .then(() => true) + .catch(() => false) + ) { + vscode.window.showErrorMessage(t("common:errors.command_already_exists", { commandName })) + break + } + + // Create the command file with template content + const templateContent = t("common:errors.command_template_content") + + await fs.writeFile(filePath, templateContent, "utf8") + provider.log(`Created new command file: ${filePath}`) + + // Open the new file in the editor + openFile(filePath) + + // Refresh commands list + const { getCommands } = await import("../../services/command/commands") + const commands = await getCommands(provider.cwd || "") + const commandList = commands.map((command) => ({ + name: command.name, + source: command.source, + filePath: command.filePath, + })) + await provider.postMessageToWebview({ + type: "commands", + commands: commandList, + }) + } catch (error) { + provider.log(`Error creating command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + vscode.window.showErrorMessage(t("common:errors.create_command_failed")) + } + break + } + + case "insertTextIntoTextarea": { + const text = message.text + if (text) { + // Send message to insert text into the chat textarea + await provider.postMessageToWebview({ + type: "insertTextIntoTextarea", + text: text, + }) + } + break + } } } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index cbe2c032b40..6cd97472efd 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -74,6 +74,13 @@ "share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.", "share_task_not_found": "Tasca no trobada o accés denegat.", "delete_rules_folder_failed": "Error en eliminar la carpeta de regles: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Ordre '{{name}}' no trobada", + "open_command_file": "Error en obrir el fitxer d'ordres", + "delete_command": "Error en eliminar l'ordre", + "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.", "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 95d315e1038..92dc790e1f9 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert.", "mode_import_failed": "Fehler beim Importieren des Modus: {{error}}", "delete_rules_folder_failed": "Fehler beim Löschen des Regelordners: {{rulesFolderPath}}. Fehler: {{error}}", + "command_not_found": "Befehl '{{name}}' nicht gefunden", + "open_command_file": "Fehler beim Öffnen der Befehlsdatei", + "delete_command": "Fehler beim Löschen des Befehls", + "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.", "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 4150e12f848..37a40d8742c 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Task not found or access denied.", "mode_import_failed": "Failed to import mode: {{error}}", "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Command '{{name}}' not found", + "open_command_file": "Failed to open command file", + "delete_command": "Failed to delete command", + "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.", "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 d47b95be7e6..0972b8a77e1 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Tarea no encontrada o acceso denegado.", "mode_import_failed": "Error al importar el modo: {{error}}", "delete_rules_folder_failed": "Error al eliminar la carpeta de reglas: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Comando '{{name}}' no encontrado", + "open_command_file": "Error al abrir el archivo de comandos", + "delete_command": "Error al eliminar el comando", + "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.", "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 e239358bbfc..d6b6f1faf3f 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Tâche non trouvée ou accès refusé.", "mode_import_failed": "Échec de l'importation du mode : {{error}}", "delete_rules_folder_failed": "Échec de la suppression du dossier de règles : {{rulesFolderPath}}. Erreur : {{error}}", + "command_not_found": "Commande '{{name}}' introuvable", + "open_command_file": "Échec de l'ouverture du fichier de commande", + "delete_command": "Échec de la suppression de la commande", + "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.", "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 d5ba036f632..935bc3b5f66 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।", "mode_import_failed": "मोड आयात करने में विफल: {{error}}", "delete_rules_folder_failed": "नियम फ़ोल्डर हटाने में विफल: {{rulesFolderPath}}। त्रुटि: {{error}}", + "command_not_found": "कमांड '{{name}}' नहीं मिला", + "open_command_file": "कमांड फ़ाइल खोलने में विफल", + "delete_command": "कमांड हटाने में विफल", + "no_workspace_for_project_command": "प्रोजेक्ट कमांड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला", + "command_already_exists": "कमांड \"{{commandName}}\" पहले से मौजूद है", + "create_command_failed": "कमांड बनाने में विफल", + "command_template_content": "यह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।", "claudeCode": { "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", "errorOutput": "त्रुटि आउटपुट: {{output}}", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index b261f69e242..71ed70fce86 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Tugas tidak ditemukan atau akses ditolak.", "mode_import_failed": "Gagal mengimpor mode: {{error}}", "delete_rules_folder_failed": "Gagal menghapus folder aturan: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Perintah '{{name}}' tidak ditemukan", + "open_command_file": "Gagal membuka file perintah", + "delete_command": "Gagal menghapus perintah", + "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.", "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 f5e15398edd..bcb5754f9b9 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Attività non trovata o accesso negato.", "mode_import_failed": "Importazione della modalità non riuscita: {{error}}", "delete_rules_folder_failed": "Impossibile eliminare la cartella delle regole: {{rulesFolderPath}}. Errore: {{error}}", + "command_not_found": "Comando '{{name}}' non trovato", + "open_command_file": "Impossibile aprire il file di comando", + "delete_command": "Impossibile eliminare il comando", + "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.", "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 9b43f64bcf4..2fb6a76b4cc 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。", "mode_import_failed": "モードのインポートに失敗しました:{{error}}", "delete_rules_folder_failed": "ルールフォルダの削除に失敗しました:{{rulesFolderPath}}。エラー:{{error}}", + "command_not_found": "コマンド '{{name}}' が見つかりません", + "open_command_file": "コマンドファイルを開けませんでした", + "delete_command": "コマンドの削除に失敗しました", + "no_workspace_for_project_command": "プロジェクトコマンド用のワークスペースフォルダが見つかりません", + "command_already_exists": "コマンド \"{{commandName}}\" は既に存在します", + "create_command_failed": "コマンドの作成に失敗しました", + "command_template_content": "これは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。", "claudeCode": { "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", "errorOutput": "エラー出力:{{output}}", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 2bfbacad320..e3ee789b047 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.", "mode_import_failed": "모드 가져오기 실패: {{error}}", "delete_rules_folder_failed": "규칙 폴더 삭제 실패: {{rulesFolderPath}}. 오류: {{error}}", + "command_not_found": "'{{name}}' 명령을 찾을 수 없습니다", + "open_command_file": "명령 파일을 열 수 없습니다", + "delete_command": "명령 삭제 실패", + "no_workspace_for_project_command": "프로젝트 명령용 워크스페이스 폴더를 찾을 수 없습니다", + "command_already_exists": "명령 \"{{commandName}}\"이(가) 이미 존재합니다", + "create_command_failed": "명령 생성에 실패했습니다", + "command_template_content": "이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.", "claudeCode": { "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", "errorOutput": "오류 출력: {{output}}", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 645a371754f..8cf11bf4351 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Taak niet gevonden of toegang geweigerd.", "mode_import_failed": "Importeren van modus mislukt: {{error}}", "delete_rules_folder_failed": "Kan regelmap niet verwijderen: {{rulesFolderPath}}. Fout: {{error}}", + "command_not_found": "Opdracht '{{name}}' niet gevonden", + "open_command_file": "Kan opdrachtbestand niet openen", + "delete_command": "Kan opdracht niet verwijderen", + "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.", "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 45251e1ab4c..d1872ce628b 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony.", "mode_import_failed": "Import trybu nie powiódł się: {{error}}", "delete_rules_folder_failed": "Nie udało się usunąć folderu reguł: {{rulesFolderPath}}. Błąd: {{error}}", + "command_not_found": "Polecenie '{{name}}' nie zostało znalezione", + "open_command_file": "Nie udało się otworzyć pliku polecenia", + "delete_command": "Nie udało się usunąć polecenia", + "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.", "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 29c951fb392..fd16391491f 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -75,6 +75,13 @@ "share_task_not_found": "Tarefa não encontrada ou acesso negado.", "mode_import_failed": "Falha ao importar o modo: {{error}}", "delete_rules_folder_failed": "Falha ao excluir pasta de regras: {{rulesFolderPath}}. Erro: {{error}}", + "command_not_found": "Comando '{{name}}' não encontrado", + "open_command_file": "Falha ao abrir arquivo de comando", + "delete_command": "Falha ao excluir comando", + "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.", "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 5f8fdf34c17..05ad66aedcb 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Задача не найдена или доступ запрещен.", "mode_import_failed": "Не удалось импортировать режим: {{error}}", "delete_rules_folder_failed": "Не удалось удалить папку правил: {{rulesFolderPath}}. Ошибка: {{error}}", + "command_not_found": "Команда '{{name}}' не найдена", + "open_command_file": "Не удалось открыть файл команды", + "delete_command": "Не удалось удалить команду", + "no_workspace_for_project_command": "Не найдена папка рабочего пространства для команды проекта", + "command_already_exists": "Команда \"{{commandName}}\" уже существует", + "create_command_failed": "Не удалось создать команду", + "command_template_content": "Это новая 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 c7feb38ef66..66d7666d4ad 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Görev bulunamadı veya erişim reddedildi.", "mode_import_failed": "Mod içe aktarılamadı: {{error}}", "delete_rules_folder_failed": "Kurallar klasörü silinemedi: {{rulesFolderPath}}. Hata: {{error}}", + "command_not_found": "'{{name}}' komutu bulunamadı", + "open_command_file": "Komut dosyası açılamadı", + "delete_command": "Komut silinemedi", + "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.", "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 84b8b409dc1..59e1e3d14b4 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -71,6 +71,13 @@ "share_task_not_found": "Không tìm thấy nhiệm vụ hoặc truy cập bị từ chối.", "mode_import_failed": "Nhập chế độ thất bại: {{error}}", "delete_rules_folder_failed": "Không thể xóa thư mục quy tắc: {{rulesFolderPath}}. Lỗi: {{error}}", + "command_not_found": "Không tìm thấy lệnh '{{name}}'", + "open_command_file": "Không thể mở tệp lệnh", + "delete_command": "Không thể xóa lệnh", + "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.", "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 7798a8bbdb1..97f7c74bb34 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -76,6 +76,13 @@ "share_task_not_found": "未找到任务或访问被拒绝。", "mode_import_failed": "导入模式失败:{{error}}", "delete_rules_folder_failed": "删除规则文件夹失败:{{rulesFolderPath}}。错误:{{error}}", + "command_not_found": "未找到命令 '{{name}}'", + "open_command_file": "打开命令文件失败", + "delete_command": "删除命令失败", + "no_workspace_for_project_command": "未找到项目命令的工作区文件夹", + "command_already_exists": "命令 \"{{commandName}}\" 已存在", + "create_command_failed": "创建命令失败", + "command_template_content": "这是一个新的斜杠命令。编辑此文件以自定义命令行为。", "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 c6105c2bf9f..2b15dd40b94 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -70,6 +70,13 @@ "share_not_enabled": "此組織未啟用工作分享功能。", "share_task_not_found": "未找到工作或存取被拒絕。", "delete_rules_folder_failed": "刪除規則資料夾失敗: {{rulesFolderPath}}。錯誤: {{error}}", + "command_not_found": "找不到指令 '{{name}}'", + "open_command_file": "開啟指令檔案失敗", + "delete_command": "刪除指令失敗", + "no_workspace_for_project_command": "找不到專案指令的工作區資料夾", + "command_already_exists": "指令 \"{{commandName}}\" 已存在", + "create_command_failed": "建立指令失敗", + "command_template_content": "這是一個新的斜線指令。編輯此檔案以自訂指令行為。", "claudeCode": { "processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。", "errorOutput": "錯誤輸出:{{output}}", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 816069f91f9..a592c3e885a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -23,6 +23,7 @@ import type { MarketplaceItem } from "@roo-code/types" export interface Command { name: string source: "global" | "project" + filePath?: string } // Type for marketplace installed metadata @@ -116,6 +117,7 @@ export interface ExtensionMessage { | "showDeleteMessageDialog" | "showEditMessageDialog" | "commands" + | "insertTextIntoTextarea" text?: string payload?: any // Add a generic payload for now, can refine later action?: diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1304e4c7d51..9c80bc1e7cf 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -202,6 +202,10 @@ export interface WebviewMessage { | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" | "requestCommands" + | "openCommandFile" + | "deleteCommand" + | "createCommand" + | "insertTextIntoTextarea" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" diff --git a/webview-ui/src/__tests__/command-autocomplete.spec.ts b/webview-ui/src/__tests__/command-autocomplete.spec.ts index d680fe7af2a..3789ad1bf50 100644 --- a/webview-ui/src/__tests__/command-autocomplete.spec.ts +++ b/webview-ui/src/__tests__/command-autocomplete.spec.ts @@ -16,22 +16,18 @@ describe("Command Autocomplete", () => { { type: ContextMenuOptionType.Problems, value: "problems" }, ] - // Mock translation function - const mockT = (key: string, options?: { name?: string }) => { - if (key === "chat:command.triggerDescription") { - return `Trigger the ${options?.name || "command"} command` - } - return key - } - describe("slash command command suggestions", () => { it('should return all commands when query is just "/"', () => { - const options = getContextMenuOptions("/", "/", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], [], mockCommands) - expect(options).toHaveLength(5) - expect(options.every((option) => option.type === ContextMenuOptionType.Command)).toBe(true) + // Should have 6 items: 1 section header + 5 commands + expect(options).toHaveLength(6) - const commandNames = options.map((option) => option.value) + // Filter out section headers to check commands + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions).toHaveLength(5) + + const commandNames = commandOptions.map((option) => option.value) expect(commandNames).toContain("setup") expect(commandNames).toContain("build") expect(commandNames).toContain("deploy") @@ -40,7 +36,7 @@ describe("Command Autocomplete", () => { }) it("should filter commands based on fuzzy search", () => { - const options = getContextMenuOptions("/set", "/set", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/set", "/set", null, mockQueryItems, [], [], mockCommands) // Should match 'setup' (fuzzy search behavior may vary) expect(options.length).toBeGreaterThan(0) @@ -50,18 +46,17 @@ describe("Command Autocomplete", () => { }) it("should return commands with correct format", () => { - const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands) const setupOption = options.find((option) => option.value === "setup") expect(setupOption).toBeDefined() expect(setupOption!.type).toBe(ContextMenuOptionType.Command) - expect(setupOption!.label).toBe("setup") - expect(setupOption!.description).toBe("Trigger the setup command") - expect(setupOption!.icon).toBe("$(play)") + expect(setupOption!.slashCommand).toBe("/setup") + expect(setupOption!.value).toBe("setup") }) it("should handle empty command list", () => { - const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], []) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], []) // Should return NoResults when no commands match expect(options).toHaveLength(1) @@ -72,7 +67,6 @@ describe("Command Autocomplete", () => { const options = getContextMenuOptions( "/nonexistent", "/nonexistent", - mockT, null, mockQueryItems, [], @@ -86,7 +80,7 @@ describe("Command Autocomplete", () => { }) it("should not return command suggestions for non-slash queries", () => { - const options = getContextMenuOptions("setup", "setup", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("setup", "setup", null, mockQueryItems, [], [], mockCommands) // Should not contain command options for non-slash queries const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) @@ -100,24 +94,15 @@ describe("Command Autocomplete", () => { { name: "deploy.prod", source: "global" }, ] - const options = getContextMenuOptions( - "/setup", - "/setup", - mockT, - null, - mockQueryItems, - [], - [], - specialCommands, - ) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], specialCommands) const setupDevOption = options.find((option) => option.value === "setup-dev") expect(setupDevOption).toBeDefined() - expect(setupDevOption!.label).toBe("setup-dev") + expect(setupDevOption!.slashCommand).toBe("/setup-dev") }) it("should handle case-insensitive fuzzy matching", () => { - const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands) const commandNames = options.map((option) => option.value) expect(commandNames).toContain("setup") @@ -133,7 +118,6 @@ describe("Command Autocomplete", () => { const options = getContextMenuOptions( "/test", "/test", - mockT, null, mockQueryItems, [], @@ -141,12 +125,13 @@ describe("Command Autocomplete", () => { commandsWithSimilarNames, ) - // 'test' should be first due to exact match - expect(options[0].value).toBe("test") + // Filter out section headers and check the first command + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions[0].value).toBe("test") }) it("should handle partial matches correctly", () => { - const options = getContextMenuOptions("/te", "/te", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/te", "/te", null, mockQueryItems, [], [], mockCommands) // Should match 'test-suite' const commandNames = options.map((option) => option.value) @@ -173,7 +158,7 @@ describe("Command Autocomplete", () => { ] as any[] it("should return both modes and commands for slash commands", () => { - const options = getContextMenuOptions("/", "/", mockT, null, mockQueryItems, [], mockModes, mockCommands) + const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], mockModes, mockCommands) const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode) const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) @@ -183,16 +168,7 @@ describe("Command Autocomplete", () => { }) it("should filter both modes and commands based on query", () => { - const options = getContextMenuOptions( - "/co", - "/co", - mockT, - null, - mockQueryItems, - [], - mockModes, - mockCommands, - ) + const options = getContextMenuOptions("/co", "/co", null, mockQueryItems, [], mockModes, mockCommands) // Should match 'code' mode and possibly some commands (fuzzy search may match) const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode) @@ -207,28 +183,30 @@ describe("Command Autocomplete", () => { describe("command source indication", () => { it("should not expose source information in autocomplete", () => { - const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands) const setupOption = options.find((option) => option.value === "setup") expect(setupOption).toBeDefined() // Source should not be exposed in the UI - expect(setupOption!.description).not.toContain("project") - expect(setupOption!.description).not.toContain("global") - expect(setupOption!.description).toBe("Trigger the setup command") + if (setupOption!.description) { + expect(setupOption!.description).not.toContain("project") + expect(setupOption!.description).not.toContain("global") + expect(setupOption!.description).toBe("Trigger the setup command") + } }) }) describe("edge cases", () => { it("should handle undefined commands gracefully", () => { - const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], undefined) + const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], undefined) expect(options).toHaveLength(1) expect(options[0].type).toBe(ContextMenuOptionType.NoResults) }) it("should handle empty query with commands", () => { - const options = getContextMenuOptions("", "", mockT, null, mockQueryItems, [], [], mockCommands) + const options = getContextMenuOptions("", "", null, mockQueryItems, [], [], mockCommands) // Should not return command options for empty query const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) @@ -240,19 +218,12 @@ describe("Command Autocomplete", () => { { name: "very-long-command-name-that-exceeds-normal-length", source: "project" }, ] - const options = getContextMenuOptions( - "/very", - "/very", - mockT, - null, - mockQueryItems, - [], - [], - longNameCommands, - ) + const options = getContextMenuOptions("/very", "/very", null, mockQueryItems, [], [], longNameCommands) - expect(options.length).toBe(1) - expect(options[0].value).toBe("very-long-command-name-that-exceeds-normal-length") + // Should have 2 items: 1 section header + 1 command + expect(options.length).toBe(2) + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions[0].value).toBe("very-long-command-name-that-exceeds-normal-length") }) it("should handle commands with numeric names", () => { @@ -262,7 +233,7 @@ describe("Command Autocomplete", () => { { name: "123test", source: "project" }, ] - const options = getContextMenuOptions("/v", "/v", mockT, null, mockQueryItems, [], [], numericCommands) + const options = getContextMenuOptions("/v", "/v", null, mockQueryItems, [], [], numericCommands) const commandNames = options.map((option) => option.value) expect(commandNames).toContain("v2-setup") diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e387732197e..29d9fe61a61 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -28,6 +28,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" +import { SlashCommandsPopover } from "./SlashCommandsPopover" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" import { EditModeControls } from "./EditModeControls" @@ -145,6 +146,36 @@ const ChatTextArea = forwardRef( } setIsEnhancingPrompt(false) + } else if (message.type === "insertTextIntoTextarea") { + if (message.text && textAreaRef.current) { + // Insert the command text at the current cursor position + const textarea = textAreaRef.current + const currentValue = inputValue + const cursorPos = textarea.selectionStart || 0 + + // Check if we need to add a space before the command + const textBefore = currentValue.slice(0, cursorPos) + const needsSpaceBefore = textBefore.length > 0 && !textBefore.endsWith(" ") + const prefix = needsSpaceBefore ? " " : "" + + // Insert the text at cursor position + const newValue = + currentValue.slice(0, cursorPos) + + prefix + + message.text + + " " + + currentValue.slice(cursorPos) + setInputValue(newValue) + + // Set cursor position after the inserted text + const newCursorPos = cursorPos + prefix.length + message.text.length + 1 + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.focus() + textAreaRef.current.setSelectionRange(newCursorPos, newCursorPos) + } + }, 0) + } } else if (message.type === "commitSearchResults") { const commits = message.commits.map((commit: any) => ({ type: ContextMenuOptionType.Git, @@ -165,7 +196,7 @@ const ChatTextArea = forwardRef( window.addEventListener("message", messageHandler) return () => window.removeEventListener("message", messageHandler) - }, [setInputValue, searchRequestId]) + }, [setInputValue, searchRequestId, inputValue]) const [isDraggingOver, setIsDraggingOver] = useState(false) const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) @@ -365,7 +396,6 @@ const ChatTextArea = forwardRef( const options = getContextMenuOptions( searchQuery, inputValue, - t, selectedType, queryItems, fileSearchResults, @@ -404,7 +434,6 @@ const ChatTextArea = forwardRef( const selectedOption = getContextMenuOptions( searchQuery, inputValue, - t, selectedType, queryItems, fileSearchResults, @@ -498,7 +527,6 @@ const ChatTextArea = forwardRef( handleHistoryNavigation, resetHistoryNavigation, commands, - t, ], ) @@ -897,6 +925,7 @@ const ChatTextArea = forwardRef( )} + + + + + + + + + ) +} diff --git a/webview-ui/src/components/chat/SlashCommandsList.tsx b/webview-ui/src/components/chat/SlashCommandsList.tsx new file mode 100644 index 00000000000..a80f4477223 --- /dev/null +++ b/webview-ui/src/components/chat/SlashCommandsList.tsx @@ -0,0 +1,203 @@ +import React, { useState } from "react" +import { Plus, Globe, Folder } from "lucide-react" + +import type { Command } from "@roo/ExtensionMessage" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, +} from "@/components/ui" +import { vscode } from "@/utils/vscode" + +import { SlashCommandItem } from "./SlashCommandItem" + +interface SlashCommandsListProps { + commands: Command[] + onRefresh: () => void +} + +export const SlashCommandsList: React.FC = ({ commands, onRefresh }) => { + const { t } = useAppTranslation() + const { cwd } = useExtensionState() + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [commandToDelete, setCommandToDelete] = useState(null) + const [globalNewName, setGlobalNewName] = useState("") + const [workspaceNewName, setWorkspaceNewName] = useState("") + + // Check if we're in a workspace/project + const hasWorkspace = Boolean(cwd) + + const handleDeleteClick = (command: Command) => { + setCommandToDelete(command) + setDeleteDialogOpen(true) + } + + const handleDeleteConfirm = () => { + if (commandToDelete) { + vscode.postMessage({ + type: "deleteCommand", + text: commandToDelete.name, + values: { source: commandToDelete.source }, + }) + setDeleteDialogOpen(false) + setCommandToDelete(null) + // Refresh the commands list after deletion + setTimeout(onRefresh, 100) + } + } + + const handleDeleteCancel = () => { + setDeleteDialogOpen(false) + setCommandToDelete(null) + } + + const handleCreateCommand = (source: "global" | "project", name: string) => { + if (!name.trim()) return + + // Append .md if not already present + const fileName = name.trim().endsWith(".md") ? name.trim() : `${name.trim()}.md` + + vscode.postMessage({ + type: "createCommand", + text: fileName, + values: { source }, + }) + + // Clear the input and refresh + if (source === "global") { + setGlobalNewName("") + } else { + setWorkspaceNewName("") + } + setTimeout(onRefresh, 500) + } + + const handleCommandClick = (command: Command) => { + // Insert the command into the textarea + vscode.postMessage({ + type: "insertTextIntoTextarea", + text: `/${command.name}`, + }) + } + + // Group commands by source + const globalCommands = commands.filter((cmd) => cmd.source === "global") + const projectCommands = commands.filter((cmd) => cmd.source === "project") + + return ( + <> + {/* Commands list */} +
+
+ {/* Global Commands Section */} +
+ + {t("chat:slashCommands.globalCommands")} +
+ {globalCommands.map((command) => ( + + ))} + {/* New global command input */} +
+ setGlobalNewName(e.target.value)} + placeholder={t("chat:slashCommands.newGlobalCommandPlaceholder")} + className="flex-1 bg-transparent text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border-none outline-none focus:outline-0 text-sm" + tabIndex={-1} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateCommand("global", globalNewName) + } + }} + /> + +
+ + {/* Workspace Commands Section - Only show if in a workspace */} + {hasWorkspace && ( + <> +
+ + {t("chat:slashCommands.workspaceCommands")} +
+ {projectCommands.map((command) => ( + + ))} + {/* New workspace command input */} +
+ setWorkspaceNewName(e.target.value)} + placeholder={t("chat:slashCommands.newWorkspaceCommandPlaceholder")} + className="flex-1 bg-transparent text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border-none outline-none focus:outline-0 text-sm" + tabIndex={-1} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateCommand("project", workspaceNewName) + } + }} + /> + +
+ + )} +
+
+ + + + + {t("chat:slashCommands.deleteDialog.title")} + + {t("chat:slashCommands.deleteDialog.description", { name: commandToDelete?.name })} + + + + + {t("chat:slashCommands.deleteDialog.cancel")} + + + {t("chat:slashCommands.deleteDialog.confirm")} + + + + + + ) +} diff --git a/webview-ui/src/components/chat/SlashCommandsPopover.tsx b/webview-ui/src/components/chat/SlashCommandsPopover.tsx new file mode 100644 index 00000000000..fc17760fc18 --- /dev/null +++ b/webview-ui/src/components/chat/SlashCommandsPopover.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from "react" +import { Zap } from "lucide-react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { cn } from "@/lib/utils" +import { vscode } from "@/utils/vscode" + +import { SlashCommandsList } from "./SlashCommandsList" + +interface SlashCommandsPopoverProps { + className?: string +} + +export const SlashCommandsPopover: React.FC = ({ className }) => { + const { t } = useAppTranslation() + const { commands } = useExtensionState() + const [isOpen, setIsOpen] = useState(false) + const portalContainer = useRooPortal("roo-portal") + + // Request commands when popover opens + useEffect(() => { + if (isOpen && (!commands || commands.length === 0)) { + handleRefresh() + } + }, [isOpen, commands]) + + const handleRefresh = () => { + vscode.postMessage({ type: "requestCommands" }) + } + + const handleOpenChange = (open: boolean) => { + setIsOpen(open) + if (open) { + // Always refresh when opening to get latest commands + handleRefresh() + } + } + + const trigger = ( + + + + ) + + return ( + + {trigger} + + +
+ {/* Header section */} +
+

+ {t("chat:slashCommands.description")} +

+
+ + {/* Commands list */} + +
+
+
+ ) +} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 381576ad481..b20b280ce67 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -108,7 +108,7 @@ "stopTts": "Atura la síntesi de veu", "typeMessage": "Escriu un missatge...", "typeTask": "Escriu la teva tasca aquí...", - "addContext": "@ per afegir context, / per canviar de mode", + "addContext": "@ per afegir context, / per a comandes", "dragFiles": "manté premut shift per arrossegar fitxers", "dragFilesImages": "manté premut shift per arrossegar fitxers/imatges", "enhancePromptDescription": "El botó 'Millora la sol·licitud' ajuda a millorar la teva sol·licitud proporcionant context addicional, aclariments o reformulacions. Prova d'escriure una sol·licitud aquí i fes clic al botó de nou per veure com funciona.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Activa la comanda {{name}}" }, + "slashCommands": { + "tooltip": "Gestionar ordres de barra", + "title": "Ordres de Barra", + "description": "Crea ordres de barra personalitzades per accedir ràpidament a indicacions i fluxos de treball utilitzats amb freqüència.", + "globalCommands": "Ordres Globals", + "workspaceCommands": "Ordres de l'Espai de Treball", + "globalCommand": "Ordre global", + "editCommand": "Editar ordre", + "deleteCommand": "Eliminar ordre", + "newGlobalCommandPlaceholder": "Nova ordre global...", + "newWorkspaceCommandPlaceholder": "Nova ordre de l'espai de treball...", + "deleteDialog": { + "title": "Eliminar Ordre", + "description": "Estàs segur que vols eliminar l'ordre \"{{name}}\"? Aquesta acció no es pot desfer.", + "cancel": "Cancel·lar", + "confirm": "Eliminar" + } + }, "queuedMessages": { "title": "Missatges en cua:", "clickToEdit": "Feu clic per editar el missatge" diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 3d43ad90a1e..a273bcf3b55 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -108,7 +108,7 @@ "stopTts": "Text-in-Sprache beenden", "typeMessage": "Nachricht eingeben...", "typeTask": "Gib deine Aufgabe hier ein...", - "addContext": "@ für Kontext, / zum Moduswechsel", + "addContext": "@ für Kontext, / für Befehle", "dragFiles": "Shift halten, um Dateien einzufügen", "dragFilesImages": "Shift halten, um Dateien/Bilder einzufügen", "enhancePromptDescription": "Die Schaltfläche 'Prompt verbessern' hilft, deine Anfrage durch zusätzlichen Kontext, Klarstellungen oder Umformulierungen zu verbessern. Versuche, hier eine Anfrage einzugeben und klicke erneut auf die Schaltfläche, um zu sehen, wie es funktioniert.", @@ -352,6 +352,24 @@ "editMessage": { "placeholder": "Bearbeite deine Nachricht..." }, + "slashCommands": { + "tooltip": "Slash-Befehle verwalten", + "title": "Slash-Befehle", + "description": "Erstelle benutzerdefinierte Slash-Befehle für schnellen Zugriff auf häufig verwendete Prompts und Workflows.", + "globalCommands": "Globale Befehle", + "workspaceCommands": "Arbeitsbereich-Befehle", + "globalCommand": "Globaler Befehl", + "editCommand": "Befehl bearbeiten", + "deleteCommand": "Befehl löschen", + "newGlobalCommandPlaceholder": "Neuer globaler Befehl...", + "newWorkspaceCommandPlaceholder": "Neuer Arbeitsbereich-Befehl...", + "deleteDialog": { + "title": "Befehl löschen", + "description": "Bist du sicher, dass du den Befehl \"{{name}}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "cancel": "Abbrechen", + "confirm": "Löschen" + } + }, "queuedMessages": { "title": "Warteschlange Nachrichten:", "clickToEdit": "Klicken zum Bearbeiten der Nachricht" diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 91568f7e36c..fed2ec6f43c 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -128,7 +128,7 @@ "stopTts": "Stop text-to-speech", "typeMessage": "Type a message...", "typeTask": "Type your task here...", - "addContext": "@ to add context, / to switch modes", + "addContext": "@ to add context, / for commands", "dragFiles": "hold shift to drag in files", "dragFilesImages": "hold shift to drag in files/images", "errorReadingFile": "Error reading file:", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Trigger the {{name}} command" }, + "slashCommands": { + "tooltip": "Manage slash commands", + "title": "Slash Commands", + "description": "Create custom slash commands for quick access to frequently used prompts and workflows.", + "globalCommands": "Global Commands", + "workspaceCommands": "Workspace Commands", + "globalCommand": "Global command", + "editCommand": "Edit command", + "deleteCommand": "Delete command", + "newGlobalCommandPlaceholder": "New global command...", + "newWorkspaceCommandPlaceholder": "New workspace command...", + "deleteDialog": { + "title": "Delete Command", + "description": "Are you sure you want to delete the command \"{{name}}\"? This action cannot be undone.", + "cancel": "Cancel", + "confirm": "Delete" + } + }, "queuedMessages": { "title": "Queued Messages:", "clickToEdit": "Click to edit message" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 7effb8f6caf..0a1655ccaa8 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -108,7 +108,7 @@ "stopTts": "Detener texto a voz", "typeMessage": "Escribe un mensaje...", "typeTask": "Escribe tu tarea aquí...", - "addContext": "@ para agregar contexto, / para cambiar modos", + "addContext": "@ para agregar contexto, / para comandos", "dragFiles": "mantén shift para arrastrar archivos", "dragFilesImages": "mantén shift para arrastrar archivos/imágenes", "enhancePromptDescription": "El botón 'Mejorar el mensaje' ayuda a mejorar tu petición proporcionando contexto adicional, aclaraciones o reformulaciones. Intenta escribir una petición aquí y haz clic en el botón nuevamente para ver cómo funciona.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Activar el comando {{name}}" }, + "slashCommands": { + "tooltip": "Gestionar comandos de barra", + "title": "Comandos de Barra", + "description": "Crea comandos de barra personalizados para acceder rápidamente a prompts y flujos de trabajo utilizados con frecuencia.", + "globalCommands": "Comandos Globales", + "workspaceCommands": "Comandos del Espacio de Trabajo", + "globalCommand": "Comando global", + "editCommand": "Editar comando", + "deleteCommand": "Eliminar comando", + "newGlobalCommandPlaceholder": "Nuevo comando global...", + "newWorkspaceCommandPlaceholder": "Nuevo comando del espacio de trabajo...", + "deleteDialog": { + "title": "Eliminar Comando", + "description": "¿Estás seguro de que quieres eliminar el comando \"{{name}}\"? Esta acción no se puede deshacer.", + "cancel": "Cancelar", + "confirm": "Eliminar" + } + }, "queuedMessages": { "title": "Mensajes en cola:", "clickToEdit": "Haz clic para editar el mensaje" diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index bc677bc7701..a8b022f2398 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -108,7 +108,7 @@ "stopTts": "Arrêter la synthèse vocale", "typeMessage": "Écrivez un message...", "typeTask": "Écrivez votre tâche ici...", - "addContext": "@ pour ajouter du contexte, / pour changer de mode", + "addContext": "@ pour ajouter du contexte, / pour les commandes", "dragFiles": "maintenir Maj pour glisser des fichiers", "dragFilesImages": "maintenir Maj pour glisser des fichiers/images", "enhancePromptDescription": "Le bouton 'Améliorer la requête' aide à améliorer votre demande en fournissant un contexte supplémentaire, des clarifications ou des reformulations. Essayez de taper une demande ici et cliquez à nouveau sur le bouton pour voir comment cela fonctionne.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Déclencher la commande {{name}}" }, + "slashCommands": { + "tooltip": "Gérer les commandes slash", + "title": "Commandes Slash", + "description": "Créez des commandes slash personnalisées pour accéder rapidement aux prompts et flux de travail fréquemment utilisés.", + "globalCommands": "Commandes Globales", + "workspaceCommands": "Commandes de l'Espace de Travail", + "globalCommand": "Commande globale", + "editCommand": "Modifier la commande", + "deleteCommand": "Supprimer la commande", + "newGlobalCommandPlaceholder": "Nouvelle commande globale...", + "newWorkspaceCommandPlaceholder": "Nouvelle commande de l'espace de travail...", + "deleteDialog": { + "title": "Supprimer la commande", + "description": "Êtes-vous sûr de vouloir supprimer la commande \"{{name}}\" ? Cette action ne peut pas être annulée.", + "cancel": "Annuler", + "confirm": "Supprimer" + } + }, "queuedMessages": { "title": "Messages en file d'attente :", "clickToEdit": "Cliquez pour modifier le message" diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index d050373c469..163dbaa81bc 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -108,7 +108,7 @@ "stopTts": "टेक्स्ट-टू-स्पीच बंद करें", "typeMessage": "एक संदेश लिखें...", "typeTask": "अपना कार्य यहां लिखें...", - "addContext": "संदर्भ जोड़ने के लिए @, मोड बदलने के लिए /", + "addContext": "संदर्भ जोड़ने के लिए @, कमांड के लिए /", "dragFiles": "फ़ाइलें खींचने के लिए shift दबाकर रखें", "dragFilesImages": "फ़ाइलें/चित्र खींचने के लिए shift दबाकर रखें", "enhancePromptDescription": "'प्रॉम्प्ट बढ़ाएँ' बटन अतिरिक्त संदर्भ, स्पष्टीकरण या पुनर्विचार प्रदान करके आपके अनुरोध को बेहतर बनाने में मदद करता है। यहां अनुरोध लिखकर देखें और यह कैसे काम करता है यह देखने के लिए बटन पर फिर से क्लिक करें।", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "{{name}} कमांड को ट्रिगर करें" }, + "slashCommands": { + "tooltip": "स्लैश कमांड प्रबंधित करें", + "title": "स्लैश कमांड", + "description": "बार-बार उपयोग किए जाने वाले प्रॉम्प्ट और वर्कफ़्लो तक त्वरित पहुंच के लिए कस्टम स्लैश कमांड बनाएं।", + "globalCommands": "वैश्विक कमांड", + "workspaceCommands": "कार्यक्षेत्र कमांड", + "globalCommand": "वैश्विक कमांड", + "editCommand": "कमांड संपादित करें", + "deleteCommand": "कमांड हटाएं", + "newGlobalCommandPlaceholder": "नया वैश्विक कमांड...", + "newWorkspaceCommandPlaceholder": "नया कार्यक्षेत्र कमांड...", + "deleteDialog": { + "title": "कमांड हटाएं", + "description": "क्या आप वाकई \"{{name}}\" कमांड को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।", + "cancel": "रद्द करें", + "confirm": "हटाएं" + } + }, "queuedMessages": { "title": "कतार में संदेश:", "clickToEdit": "संदेश संपादित करने के लिए क्लिक करें" diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index ed6109e47be..2ee922887cc 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -131,7 +131,7 @@ "stopTts": "Hentikan text-to-speech", "typeMessage": "Ketik pesan...", "typeTask": "Bangun, cari, tanya sesuatu", - "addContext": "@ untuk menambah konteks, / untuk ganti mode", + "addContext": "@ untuk menambah konteks, / untuk perintah", "dragFiles": "tahan shift untuk drag file", "dragFilesImages": "tahan shift untuk drag file/gambar", "errorReadingFile": "Error membaca file:", @@ -358,6 +358,24 @@ "command": { "triggerDescription": "Jalankan perintah {{name}}" }, + "slashCommands": { + "tooltip": "Kelola perintah slash", + "title": "Perintah Slash", + "description": "Buat perintah slash kustom untuk akses cepat ke prompt dan alur kerja yang sering digunakan.", + "globalCommands": "Perintah Global", + "workspaceCommands": "Perintah Workspace", + "globalCommand": "Perintah global", + "editCommand": "Edit perintah", + "deleteCommand": "Hapus perintah", + "newGlobalCommandPlaceholder": "Perintah global baru...", + "newWorkspaceCommandPlaceholder": "Perintah workspace baru...", + "deleteDialog": { + "title": "Hapus Perintah", + "description": "Apakah Anda yakin ingin menghapus perintah \"{{name}}\"? Tindakan ini tidak dapat dibatalkan.", + "cancel": "Batal", + "confirm": "Hapus" + } + }, "queuedMessages": { "title": "Pesan Antrian:", "clickToEdit": "Klik untuk mengedit pesan" diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index c35209a8cb4..314261d95dc 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -108,7 +108,7 @@ "stopTts": "Interrompi sintesi vocale", "typeMessage": "Scrivi un messaggio...", "typeTask": "Scrivi la tua attività qui...", - "addContext": "@ per aggiungere contesto, / per cambiare modalità", + "addContext": "@ per aggiungere contesto, / per i comandi", "dragFiles": "tieni premuto shift per trascinare file", "dragFilesImages": "tieni premuto shift per trascinare file/immagini", "enhancePromptDescription": "Il pulsante 'Migliora prompt' aiuta a migliorare la tua richiesta fornendo contesto aggiuntivo, chiarimenti o riformulazioni. Prova a digitare una richiesta qui e fai di nuovo clic sul pulsante per vedere come funziona.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Attiva il comando {{name}}" }, + "slashCommands": { + "tooltip": "Gestisci comandi slash", + "title": "Comandi Slash", + "description": "Crea comandi slash personalizzati per accedere rapidamente a prompt e flussi di lavoro utilizzati frequentemente.", + "globalCommands": "Comandi Globali", + "workspaceCommands": "Comandi dello Spazio di Lavoro", + "globalCommand": "Comando globale", + "editCommand": "Modifica comando", + "deleteCommand": "Elimina comando", + "newGlobalCommandPlaceholder": "Nuovo comando globale...", + "newWorkspaceCommandPlaceholder": "Nuovo comando dello spazio di lavoro...", + "deleteDialog": { + "title": "Elimina Comando", + "description": "Sei sicuro di voler eliminare il comando \"{{name}}\"? Questa azione non può essere annullata.", + "cancel": "Annulla", + "confirm": "Elimina" + } + }, "queuedMessages": { "title": "Messaggi in coda:", "clickToEdit": "Clicca per modificare il messaggio" diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 82fded799b5..b2b37e60596 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -108,7 +108,7 @@ "stopTts": "テキスト読み上げを停止", "typeMessage": "メッセージを入力...", "typeTask": "ここにタスクを入力...", - "addContext": "コンテキスト追加は@、モード切替は/", + "addContext": "コンテキスト追加は@、コマンドは/", "dragFiles": "ファイルをドラッグするにはShiftキーを押したまま", "dragFilesImages": "ファイル/画像をドラッグするにはShiftキーを押したまま", "enhancePromptDescription": "「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "{{name}}コマンドをトリガー" }, + "slashCommands": { + "tooltip": "スラッシュコマンドを管理", + "title": "スラッシュコマンド", + "description": "よく使用するプロンプトやワークフローに素早くアクセスするためのカスタムスラッシュコマンドを作成します。", + "globalCommands": "グローバルコマンド", + "workspaceCommands": "ワークスペースコマンド", + "globalCommand": "グローバルコマンド", + "editCommand": "コマンドを編集", + "deleteCommand": "コマンドを削除", + "newGlobalCommandPlaceholder": "新しいグローバルコマンド...", + "newWorkspaceCommandPlaceholder": "新しいワークスペースコマンド...", + "deleteDialog": { + "title": "コマンドを削除", + "description": "\"{{name}}\" コマンドを削除してもよろしいですか?この操作は元に戻せません。", + "cancel": "キャンセル", + "confirm": "削除" + } + }, "queuedMessages": { "title": "キューメッセージ:", "clickToEdit": "クリックしてメッセージを編集" diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 0432ce56522..07a15bfc8d0 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -108,7 +108,7 @@ "stopTts": "텍스트 음성 변환 중지", "typeMessage": "메시지 입력...", "typeTask": "여기에 작업 입력...", - "addContext": "컨텍스트 추가는 @, 모드 전환은 /", + "addContext": "컨텍스트 추가는 @, 명령어는 /", "dragFiles": "파일을 드래그하려면 shift 키 누르기", "dragFilesImages": "파일/이미지를 드래그하려면 shift 키 누르기", "enhancePromptDescription": "'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "{{name}} 명령 트리거" }, + "slashCommands": { + "tooltip": "슬래시 명령 관리", + "title": "슬래시 명령", + "description": "자주 사용하는 프롬프트와 워크플로우에 빠르게 액세스할 수 있는 사용자 정의 슬래시 명령을 만듭니다.", + "globalCommands": "전역 명령", + "workspaceCommands": "작업 공간 명령", + "globalCommand": "전역 명령", + "editCommand": "명령 편집", + "deleteCommand": "명령 삭제", + "newGlobalCommandPlaceholder": "새 전역 명령...", + "newWorkspaceCommandPlaceholder": "새 작업 공간 명령...", + "deleteDialog": { + "title": "명령 삭제", + "description": "\"{{name}}\" 명령을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "cancel": "취소", + "confirm": "삭제" + } + }, "queuedMessages": { "title": "대기열 메시지:", "clickToEdit": "클릭하여 메시지 편집" diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 6dae0113af1..2ab00fe3096 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -117,7 +117,7 @@ "stopTts": "Stop tekst-naar-spraak", "typeMessage": "Typ een bericht...", "typeTask": "Typ hier je taak...", - "addContext": "@ om context toe te voegen, / om van modus te wisselen", + "addContext": "@ om context toe te voegen, / voor commando's", "dragFiles": "houd shift ingedrukt om bestanden te slepen", "dragFilesImages": "houd shift ingedrukt om bestanden/afbeeldingen te slepen", "errorReadingFile": "Fout bij het lezen van bestand:", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Activeer de {{name}} opdracht" }, + "slashCommands": { + "tooltip": "Slash-opdrachten beheren", + "title": "Slash-opdrachten", + "description": "Maak aangepaste slash-opdrachten voor snelle toegang tot veelgebruikte prompts en workflows.", + "globalCommands": "Globale Opdrachten", + "workspaceCommands": "Werkruimte Opdrachten", + "globalCommand": "Globale opdracht", + "editCommand": "Opdracht bewerken", + "deleteCommand": "Opdracht verwijderen", + "newGlobalCommandPlaceholder": "Nieuwe globale opdracht...", + "newWorkspaceCommandPlaceholder": "Nieuwe werkruimte opdracht...", + "deleteDialog": { + "title": "Opdracht Verwijderen", + "description": "Weet je zeker dat je de opdracht \"{{name}}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "cancel": "Annuleren", + "confirm": "Verwijderen" + } + }, "queuedMessages": { "title": "Berichten in wachtrij:", "clickToEdit": "Klik om bericht te bewerken" diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 984fa9a24c8..2ccef3fb3e6 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -108,7 +108,7 @@ "stopTts": "Zatrzymaj syntezę mowy", "typeMessage": "Wpisz wiadomość...", "typeTask": "Wpisz swoje zadanie tutaj...", - "addContext": "@ aby dodać kontekst, / aby zmienić tryb", + "addContext": "@ aby dodać kontekst, / dla poleceń", "dragFiles": "przytrzymaj shift, aby przeciągnąć pliki", "dragFilesImages": "przytrzymaj shift, aby przeciągnąć pliki/obrazy", "enhancePromptDescription": "Przycisk 'Ulepsz podpowiedź' pomaga ulepszyć Twoją prośbę, dostarczając dodatkowy kontekst, wyjaśnienia lub przeformułowania. Spróbuj wpisać prośbę tutaj i kliknij przycisk ponownie, aby zobaczyć, jak to działa.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Uruchom polecenie {{name}}" }, + "slashCommands": { + "tooltip": "Zarządzaj poleceniami slash", + "title": "Polecenia Slash", + "description": "Twórz niestandardowe polecenia slash dla szybkiego dostępu do często używanych promptów i przepływów pracy.", + "globalCommands": "Polecenia Globalne", + "workspaceCommands": "Polecenia Obszaru Roboczego", + "globalCommand": "Polecenie globalne", + "editCommand": "Edytuj polecenie", + "deleteCommand": "Usuń polecenie", + "newGlobalCommandPlaceholder": "Nowe polecenie globalne...", + "newWorkspaceCommandPlaceholder": "Nowe polecenie obszaru roboczego...", + "deleteDialog": { + "title": "Usuń Polecenie", + "description": "Czy na pewno chcesz usunąć polecenie \"{{name}}\"? Tej akcji nie można cofnąć.", + "cancel": "Anuluj", + "confirm": "Usuń" + } + }, "queuedMessages": { "title": "Wiadomości w kolejce:", "clickToEdit": "Kliknij, aby edytować wiadomość" diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index cbb1918e749..c1999d06f1b 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -108,7 +108,7 @@ "stopTts": "Parar conversão de texto em fala", "typeMessage": "Digite uma mensagem...", "typeTask": "Digite sua tarefa aqui...", - "addContext": "@ para adicionar contexto, / para alternar modos", + "addContext": "@ para adicionar contexto, / para comandos", "dragFiles": "segure shift para arrastar arquivos", "dragFilesImages": "segure shift para arrastar arquivos/imagens", "enhancePromptDescription": "O botão 'Aprimorar prompt' ajuda a melhorar seu pedido fornecendo contexto adicional, esclarecimentos ou reformulações. Tente digitar um pedido aqui e clique no botão novamente para ver como funciona.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Acionar o comando {{name}}" }, + "slashCommands": { + "tooltip": "Gerenciar comandos de barra", + "title": "Comandos de Barra", + "description": "Crie comandos de barra personalizados para acesso rápido a prompts e fluxos de trabalho usados com frequência.", + "globalCommands": "Comandos Globais", + "workspaceCommands": "Comandos do Espaço de Trabalho", + "globalCommand": "Comando global", + "editCommand": "Editar comando", + "deleteCommand": "Excluir comando", + "newGlobalCommandPlaceholder": "Novo comando global...", + "newWorkspaceCommandPlaceholder": "Novo comando do espaço de trabalho...", + "deleteDialog": { + "title": "Excluir Comando", + "description": "Tem certeza de que deseja excluir o comando \"{{name}}\"? Esta ação não pode ser desfeita.", + "cancel": "Cancelar", + "confirm": "Excluir" + } + }, "queuedMessages": { "title": "Mensagens na fila:", "clickToEdit": "Clique para editar a mensagem" diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 378e749405b..06c1e5a0e9d 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -117,7 +117,7 @@ "stopTts": "Остановить синтез речи", "typeMessage": "Введите сообщение...", "typeTask": "Введите вашу задачу здесь...", - "addContext": "@ для добавления контекста, / для смены режима", + "addContext": "@ для добавления контекста, / для команд", "dragFiles": "удерживайте shift для перетаскивания файлов", "dragFilesImages": "удерживайте shift для перетаскивания файлов/изображений", "errorReadingFile": "Ошибка чтения файла:", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Запустить команду {{name}}" }, + "slashCommands": { + "tooltip": "Управление слэш-командами", + "title": "Слэш-команды", + "description": "Создавайте пользовательские слэш-команды для быстрого доступа к часто используемым промптам и рабочим процессам.", + "globalCommands": "Глобальные команды", + "workspaceCommands": "Команды рабочего пространства", + "globalCommand": "Глобальная команда", + "editCommand": "Редактировать команду", + "deleteCommand": "Удалить команду", + "newGlobalCommandPlaceholder": "Новая глобальная команда...", + "newWorkspaceCommandPlaceholder": "Новая команда рабочего пространства...", + "deleteDialog": { + "title": "Удалить команду", + "description": "Вы уверены, что хотите удалить команду \"{{name}}\"? Это действие нельзя отменить.", + "cancel": "Отмена", + "confirm": "Удалить" + } + }, "queuedMessages": { "title": "Сообщения в очереди:", "clickToEdit": "Нажмите, чтобы редактировать сообщение" diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index e217184a9cc..8f1a8039733 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -108,7 +108,7 @@ "stopTts": "Metin okumayı durdur", "typeMessage": "Bir mesaj yazın...", "typeTask": "Görevinizi buraya yazın...", - "addContext": "Bağlam eklemek için @, mod değiştirmek için /", + "addContext": "Bağlam eklemek için @, komutlar için /", "dragFiles": "dosyaları sürüklemek için shift tuşuna basılı tutun", "dragFilesImages": "dosyaları/resimleri sürüklemek için shift tuşuna basılı tutun", "enhancePromptDescription": "'İstemi geliştir' düğmesi, ek bağlam, açıklama veya yeniden ifade sağlayarak isteğinizi iyileştirmeye yardımcı olur. Buraya bir istek yazıp düğmeye tekrar tıklayarak nasıl çalıştığını görebilirsiniz.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "{{name}} komutunu tetikle" }, + "slashCommands": { + "tooltip": "Eğik çizgi komutlarını yönet", + "title": "Eğik Çizgi Komutları", + "description": "Sık kullanılan komut istemleri ve iş akışlarına hızlı erişim için özel eğik çizgi komutları oluşturun.", + "globalCommands": "Genel Komutlar", + "workspaceCommands": "Çalışma Alanı Komutları", + "globalCommand": "Genel komut", + "editCommand": "Komutu düzenle", + "deleteCommand": "Komutu sil", + "newGlobalCommandPlaceholder": "Yeni genel komut...", + "newWorkspaceCommandPlaceholder": "Yeni çalışma alanı komutu...", + "deleteDialog": { + "title": "Komutu Sil", + "description": "\"{{name}}\" komutunu silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "cancel": "İptal", + "confirm": "Sil" + } + }, "queuedMessages": { "title": "Sıradaki Mesajlar:", "clickToEdit": "Mesajı düzenlemek için tıkla" diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index b6fe59d9b72..44c96af46b1 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -108,7 +108,7 @@ "stopTts": "Dừng chuyển văn bản thành giọng nói", "typeMessage": "Nhập tin nhắn...", "typeTask": "Nhập nhiệm vụ của bạn tại đây...", - "addContext": "@ để thêm ngữ cảnh, / để chuyển chế độ", + "addContext": "@ để thêm ngữ cảnh, / cho lệnh", "dragFiles": "giữ shift để kéo tệp", "dragFilesImages": "giữ shift để kéo tệp/hình ảnh", "enhancePromptDescription": "Nút 'Nâng cao yêu cầu' giúp cải thiện yêu cầu của bạn bằng cách cung cấp ngữ cảnh bổ sung, làm rõ hoặc diễn đạt lại. Hãy thử nhập yêu cầu tại đây và nhấp vào nút một lần nữa để xem cách thức hoạt động.", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "Kích hoạt lệnh {{name}}" }, + "slashCommands": { + "tooltip": "Quản lý lệnh gạch chéo", + "title": "Lệnh Gạch Chéo", + "description": "Tạo lệnh gạch chéo tùy chỉnh để truy cập nhanh vào các lời nhắc và quy trình làm việc thường dùng.", + "globalCommands": "Lệnh Toàn Cục", + "workspaceCommands": "Lệnh Không Gian Làm Việc", + "globalCommand": "Lệnh toàn cục", + "editCommand": "Chỉnh sửa lệnh", + "deleteCommand": "Xóa lệnh", + "newGlobalCommandPlaceholder": "Lệnh toàn cục mới...", + "newWorkspaceCommandPlaceholder": "Lệnh không gian làm việc mới...", + "deleteDialog": { + "title": "Xóa Lệnh", + "description": "Bạn có chắc chắn muốn xóa lệnh \"{{name}}\" không? Hành động này không thể hoàn tác.", + "cancel": "Hủy", + "confirm": "Xóa" + } + }, "queuedMessages": { "title": "Tin nhắn trong hàng đợi:", "clickToEdit": "Nhấp để chỉnh sửa tin nhắn" diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 4f87348a989..c1acb3ef417 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -108,7 +108,7 @@ "stopTts": "停止文本转语音", "typeMessage": "输入消息...", "typeTask": "在此处输入您的任务...", - "addContext": "@添加上下文,/切换模式", + "addContext": "@添加上下文,/输入命令", "dragFiles": "Shift+拖拽文件", "dragFilesImages": "Shift+拖拽文件/图片", "enhancePromptDescription": "'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。", @@ -352,6 +352,24 @@ "editMessage": { "placeholder": "编辑消息..." }, + "slashCommands": { + "tooltip": "管理斜杠命令", + "title": "斜杠命令", + "description": "创建自定义斜杠命令,快速访问常用提示词和工作流程。", + "globalCommands": "全局命令", + "workspaceCommands": "工作区命令", + "globalCommand": "全局命令", + "editCommand": "编辑命令", + "deleteCommand": "删除命令", + "newGlobalCommandPlaceholder": "新建全局命令...", + "newWorkspaceCommandPlaceholder": "新建工作区命令...", + "deleteDialog": { + "title": "删除命令", + "description": "确定要删除命令 \"{{name}}\" 吗?此操作无法撤销。", + "cancel": "取消", + "confirm": "删除" + } + }, "queuedMessages": { "title": "队列消息:", "clickToEdit": "点击编辑消息" diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0671a07f876..afa08f281f5 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -108,7 +108,7 @@ "stopTts": "停止文字轉語音", "typeMessage": "輸入訊息...", "typeTask": "在此處輸入您的工作...", - "addContext": "輸入 @ 新增內容,輸入 / 切換模式", + "addContext": "輸入 @ 新增內容,輸入 / 執行指令", "dragFiles": "按住 Shift 鍵拖曳檔案", "dragFilesImages": "按住 Shift 鍵拖曳檔案/圖片", "enhancePromptDescription": "「增強提示」按鈕透過提供額外內容、說明或重新表述來幫助改進您的請求。嘗試在此處輸入請求,然後再次點選按鈕以了解其運作方式。", @@ -352,6 +352,24 @@ "command": { "triggerDescription": "觸發 {{name}} 命令" }, + "slashCommands": { + "tooltip": "管理斜線指令", + "title": "斜線指令", + "description": "建立自訂斜線指令,快速存取常用提示詞和工作流程。", + "globalCommands": "全域指令", + "workspaceCommands": "工作區指令", + "globalCommand": "全域指令", + "editCommand": "編輯指令", + "deleteCommand": "刪除指令", + "newGlobalCommandPlaceholder": "新增全域指令...", + "newWorkspaceCommandPlaceholder": "新增工作區指令...", + "deleteDialog": { + "title": "刪除指令", + "description": "確定要刪除指令 \"{{name}}\" 嗎?此動作無法復原。", + "cancel": "取消", + "confirm": "刪除" + } + }, "queuedMessages": { "title": "佇列訊息:", "clickToEdit": "點擊編輯訊息" diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index f0266cc07f6..4fc1502892f 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -194,16 +194,8 @@ describe("getContextMenuOptions", () => { { path: "/Users/test/project/assets/", type: "folder", label: "assets/" }, ] - // Mock translation function - const mockT = (key: string, options?: { name?: string }) => { - if (key === "chat:command.triggerDescription" && options?.name) { - return `Trigger command: ${options.name}` - } - return key - } - it("should return all option types for empty query", () => { - const result = getContextMenuOptions("", "", mockT, null, []) + const result = getContextMenuOptions("", "", null, []) expect(result).toHaveLength(6) expect(result.map((item) => item.type)).toEqual([ ContextMenuOptionType.Problems, @@ -216,7 +208,7 @@ describe("getContextMenuOptions", () => { }) it("should filter by selected type when query is empty", () => { - const result = getContextMenuOptions("", "", mockT, ContextMenuOptionType.File, mockQueryItems) + const result = getContextMenuOptions("", "", ContextMenuOptionType.File, mockQueryItems) expect(result).toHaveLength(2) expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.File) expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.OpenedFile) @@ -225,19 +217,19 @@ describe("getContextMenuOptions", () => { }) it("should match git commands", () => { - const result = getContextMenuOptions("git", "git", mockT, null, mockQueryItems) + const result = getContextMenuOptions("git", "git", null, mockQueryItems) expect(result[0].type).toBe(ContextMenuOptionType.Git) expect(result[0].label).toBe("Git Commits") }) it("should match git commit hashes", () => { - const result = getContextMenuOptions("abc1234", "abc1234", mockT, null, mockQueryItems) + const result = getContextMenuOptions("abc1234", "abc1234", null, mockQueryItems) expect(result[0].type).toBe(ContextMenuOptionType.Git) expect(result[0].value).toBe("abc1234") }) it("should return NoResults when no matches found", () => { - const result = getContextMenuOptions("nonexistent", "nonexistent", mockT, null, mockQueryItems) + const result = getContextMenuOptions("nonexistent", "nonexistent", null, mockQueryItems) expect(result).toHaveLength(1) expect(result[0].type).toBe(ContextMenuOptionType.NoResults) }) @@ -258,7 +250,7 @@ describe("getContextMenuOptions", () => { }, ] - const result = getContextMenuOptions("test", "test", mockT, null, testItems, mockDynamicSearchResults) + const result = getContextMenuOptions("test", "test", null, testItems, mockDynamicSearchResults) // Check if opened files and dynamic search results are included expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true) @@ -267,7 +259,7 @@ describe("getContextMenuOptions", () => { it("should maintain correct result ordering according to implementation", () => { // Add multiple item types to test ordering - const result = getContextMenuOptions("t", "t", mockT, null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("t", "t", null, mockQueryItems, mockDynamicSearchResults) // Find the different result types const fileResults = result.filter( @@ -298,7 +290,7 @@ describe("getContextMenuOptions", () => { }) it("should include opened files when dynamic search results exist", () => { - const result = getContextMenuOptions("open", "open", mockT, null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("open", "open", null, mockQueryItems, mockDynamicSearchResults) // Verify opened files are included expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true) @@ -307,7 +299,7 @@ describe("getContextMenuOptions", () => { }) it("should include git results when dynamic search results exist", () => { - const result = getContextMenuOptions("commit", "commit", mockT, null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("commit", "commit", null, mockQueryItems, mockDynamicSearchResults) // Verify git results are included expect(result.some((item) => item.type === ContextMenuOptionType.Git)).toBe(true) @@ -328,7 +320,7 @@ describe("getContextMenuOptions", () => { }, ] - const result = getContextMenuOptions("test", "test", mockT, null, mockQueryItems, duplicateSearchResults) + const result = getContextMenuOptions("test", "test", null, mockQueryItems, duplicateSearchResults) // Count occurrences of src/test.ts in results const duplicateCount = result.filter( @@ -348,7 +340,6 @@ describe("getContextMenuOptions", () => { const result = getContextMenuOptions( "nonexistentquery123456", "nonexistentquery123456", - mockT, null, mockQueryItems, [], // Empty dynamic search results @@ -396,7 +387,7 @@ describe("getContextMenuOptions", () => { ] // Get results for "test" query - const result = getContextMenuOptions(testQuery, testQuery, mockT, null, testItems, testSearchResults) + const result = getContextMenuOptions(testQuery, testQuery, null, testItems, testSearchResults) // Verify we have results expect(result.length).toBeGreaterThan(0) @@ -442,17 +433,18 @@ describe("getContextMenuOptions", () => { }, ] - const result = getContextMenuOptions("/co", "/co", mockT, null, [], [], mockModes) + const result = getContextMenuOptions("/co", "/co", null, [], [], mockModes) - // Verify mode results are returned - expect(result[0].type).toBe(ContextMenuOptionType.Mode) - expect(result[0].value).toBe("code") + // Should have section header first, then mode results + expect(result[0].type).toBe(ContextMenuOptionType.SectionHeader) + expect(result[1].type).toBe(ContextMenuOptionType.Mode) + expect(result[1].value).toBe("code") }) it("should not process slash commands when query starts with slash but inputValue doesn't", () => { // Use a completely non-matching query to ensure we get NoResults // and provide empty query items to avoid any matches - const result = getContextMenuOptions("/nonexistentquery", "Hello /code", mockT, null, [], []) + const result = getContextMenuOptions("/nonexistentquery", "Hello /code", null, [], []) // Should not process as a mode command expect(result[0].type).not.toBe(ContextMenuOptionType.Mode) @@ -462,7 +454,7 @@ describe("getContextMenuOptions", () => { // --- Tests for Escaped Spaces (Focus on how paths are presented) --- it("should return search results with correct labels/descriptions (no escaping needed here)", () => { - const options = getContextMenuOptions("@search", "search", mockT, null, mockQueryItems, mockSearchResults) + const options = getContextMenuOptions("@search", "search", null, mockQueryItems, mockSearchResults) const fileResult = options.find((o) => o.label === "search result spaces.ts") expect(fileResult).toBeDefined() // Value should be the normalized path, description might be the same or label @@ -475,7 +467,7 @@ describe("getContextMenuOptions", () => { }) it("should return query items (like opened files) with correct labels/descriptions", () => { - const options = getContextMenuOptions("open", "@open", mockT, null, mockQueryItems, []) + const options = getContextMenuOptions("open", "@open", null, mockQueryItems, []) const openedFile = options.find((o) => o.label === "open file.ts") expect(openedFile).toBeDefined() expect(openedFile?.value).toBe("src/open file.ts") @@ -492,7 +484,7 @@ describe("getContextMenuOptions", () => { ] // The formatting happens in getContextMenuOptions when converting search results to menu items - const formattedItems = getContextMenuOptions("spaces", "@spaces", mockT, null, [], searchResults) + const formattedItems = getContextMenuOptions("spaces", "@spaces", null, [], searchResults) // Verify we get some results back that aren't "No Results" expect(formattedItems.length).toBeGreaterThan(0) diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 50ab3cf12b3..fb71e7e2a91 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -107,6 +107,7 @@ export enum ContextMenuOptionType { NoResults = "noResults", Mode = "mode", // Add mode type Command = "command", // Add command type + SectionHeader = "sectionHeader", // Add section header type } export interface ContextMenuQueryItem { @@ -115,12 +116,13 @@ export interface ContextMenuQueryItem { label?: string description?: string icon?: string + slashCommand?: string + secondaryText?: string } export function getContextMenuOptions( query: string, inputValue: string, - t: (key: string, options?: { name?: string }) => string, selectedType: ContextMenuOptionType | null = null, queryItems: ContextMenuQueryItem[], dynamicSearchResults: SearchResult[] = [], @@ -132,7 +134,42 @@ export function getContextMenuOptions( const slashQuery = query.slice(1) const results: ContextMenuQueryItem[] = [] - // Add mode suggestions + // Add command suggestions first (prioritize commands at the top) + if (commands?.length) { + // Create searchable strings array for fzf + const searchableCommands = commands.map((command) => ({ + original: command, + searchStr: command.name, + })) + + // Initialize fzf instance for fuzzy search + const fzf = new Fzf(searchableCommands, { + selector: (item) => item.searchStr, + }) + + // Get fuzzy matching commands + const matchingCommands = slashQuery + ? fzf.find(slashQuery).map((result) => ({ + type: ContextMenuOptionType.Command, + value: result.item.original.name, + slashCommand: `/${result.item.original.name}`, + })) + : commands.map((command) => ({ + type: ContextMenuOptionType.Command, + value: command.name, + slashCommand: `/${command.name}`, + })) + + if (matchingCommands.length > 0) { + results.push({ + type: ContextMenuOptionType.SectionHeader, + label: "Custom Commands", + }) + results.push(...matchingCommands) + } + } + + // Add mode suggestions second if (modes?.length) { // Create searchable strings array for fzf const searchableItems = modes.map((mode) => ({ @@ -150,50 +187,23 @@ export function getContextMenuOptions( ? fzf.find(slashQuery).map((result) => ({ type: ContextMenuOptionType.Mode, value: result.item.original.slug, - label: result.item.original.name, + slashCommand: `/${result.item.original.slug}`, description: getModeDescription(result.item.original), })) : modes.map((mode) => ({ type: ContextMenuOptionType.Mode, value: mode.slug, - label: mode.name, + slashCommand: `/${mode.slug}`, description: getModeDescription(mode), })) - results.push(...matchingModes) - } - - // Add command suggestions - if (commands?.length) { - // Create searchable strings array for fzf - const searchableCommands = commands.map((command) => ({ - original: command, - searchStr: command.name, - })) - - // Initialize fzf instance for fuzzy search - const fzf = new Fzf(searchableCommands, { - selector: (item) => item.searchStr, - }) - - // Get fuzzy matching commands - const matchingCommands = slashQuery - ? fzf.find(slashQuery).map((result) => ({ - type: ContextMenuOptionType.Command, - value: result.item.original.name, - label: result.item.original.name, - description: t("chat:command.triggerDescription", { name: result.item.original.name }), - icon: "$(play)", - })) - : commands.map((command) => ({ - type: ContextMenuOptionType.Command, - value: command.name, - label: command.name, - description: t("chat:command.triggerDescription", { name: command.name }), - icon: "$(play)", - })) - - results.push(...matchingCommands) + if (matchingModes.length > 0) { + results.push({ + type: ContextMenuOptionType.SectionHeader, + label: "Modes", + }) + results.push(...matchingModes) + } } return results.length > 0 ? results : [{ type: ContextMenuOptionType.NoResults }]