diff --git a/.github/workflows/nightly-publish.yml b/.github/workflows/nightly-publish.yml index 93471d0790..8ce4c5ca6a 100644 --- a/.github/workflows/nightly-publish.yml +++ b/.github/workflows/nightly-publish.yml @@ -1,10 +1,7 @@ name: Nightly Publish on: - workflow_run: - workflows: ["Code QA Roo Code"] - types: - - completed + push: branches: [main] workflow_dispatch: # Allows manual triggering. diff --git a/CHANGELOG.md b/CHANGELOG.md index e94c518a66..ca9b46d596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Roo Code Changelog +## [3.23.6] - 2025-07-10 + +- Grok 4 + +## [3.23.5] - 2025-07-09 + +- Fix: use decodeURIComponent in openFile (thanks @vivekfyi!) +- Fix(embeddings): Translate error messages before sending to UI (thanks @daniel-lxs!) +- Make account tab visible + +## [3.23.4] - 2025-07-09 + +- Update chat area icons for better discoverability & consistency +- Fix a bug that allowed `list_files` to return directory results that should be excluded by .gitignore +- Add an overflow header menu to make the UI a little tidier (thanks @dlab-anton) +- Fix a bug the issue where null custom modes configuration files cause a 'Cannot read properties of null' error (thanks @daniel-lxs!) +- Replace native title attributes with StandardTooltip component for consistency (thanks @daniel-lxs!) + ## [3.23.3] - 2025-07-09 - Remove erroneous line from announcement modal diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index b048c19403..c405621f82 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -277,9 +277,35 @@ export const vertexModels = { export const VERTEX_REGIONS = [ { value: "global", label: "global" }, - { value: "us-east5", label: "us-east5" }, { value: "us-central1", label: "us-central1" }, + { value: "us-east1", label: "us-east1" }, + { value: "us-east4", label: "us-east4" }, + { value: "us-east5", label: "us-east5" }, + { value: "us-west1", label: "us-west1" }, + { value: "us-west2", label: "us-west2" }, + { value: "us-west3", label: "us-west3" }, + { value: "us-west4", label: "us-west4" }, + { value: "northamerica-northeast1", label: "northamerica-northeast1" }, + { value: "northamerica-northeast2", label: "northamerica-northeast2" }, + { value: "southamerica-east1", label: "southamerica-east1" }, { value: "europe-west1", label: "europe-west1" }, + { value: "europe-west2", label: "europe-west2" }, + { value: "europe-west3", label: "europe-west3" }, { value: "europe-west4", label: "europe-west4" }, + { value: "europe-west6", label: "europe-west6" }, + { value: "europe-central2", label: "europe-central2" }, + { value: "asia-east1", label: "asia-east1" }, + { value: "asia-east2", label: "asia-east2" }, + { value: "asia-northeast1", label: "asia-northeast1" }, + { value: "asia-northeast2", label: "asia-northeast2" }, + { value: "asia-northeast3", label: "asia-northeast3" }, + { value: "asia-south1", label: "asia-south1" }, + { value: "asia-south2", label: "asia-south2" }, { value: "asia-southeast1", label: "asia-southeast1" }, + { value: "asia-southeast2", label: "asia-southeast2" }, + { value: "australia-southeast1", label: "australia-southeast1" }, + { value: "australia-southeast2", label: "australia-southeast2" }, + { value: "me-west1", label: "me-west1" }, + { value: "me-central1", label: "me-central1" }, + { value: "africa-south1", label: "africa-south1" }, ] diff --git a/packages/types/src/providers/xai.ts b/packages/types/src/providers/xai.ts index 4f2cedd14b..12dc8a145c 100644 --- a/packages/types/src/providers/xai.ts +++ b/packages/types/src/providers/xai.ts @@ -3,26 +3,19 @@ import type { ModelInfo } from "../model.js" // https://docs.x.ai/docs/api-reference export type XAIModelId = keyof typeof xaiModels -export const xaiDefaultModelId: XAIModelId = "grok-3" +export const xaiDefaultModelId: XAIModelId = "grok-4" export const xaiModels = { - "grok-2-1212": { + "grok-4": { maxTokens: 8192, - contextWindow: 131072, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 2.0, - outputPrice: 10.0, - description: "xAI's Grok-2 model (version 1212) with 128K context window", - }, - "grok-2-vision-1212": { - maxTokens: 8192, - contextWindow: 32768, + contextWindow: 256000, supportsImages: true, - supportsPromptCache: false, - inputPrice: 2.0, - outputPrice: 10.0, - description: "xAI's Grok-2 Vision model (version 1212) with image support and 32K context window", + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 0.75, + cacheReadsPrice: 0.75, + description: "xAI's Grok-4 model with 256K context window", }, "grok-3": { maxTokens: 8192, @@ -70,4 +63,22 @@ export const xaiModels = { description: "xAI's Grok-3 mini fast model with 128K context window", supportsReasoningEffort: true, }, + "grok-2-1212": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 10.0, + description: "xAI's Grok-2 model (version 1212) with 128K context window", + }, + "grok-2-vision-1212": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 10.0, + description: "xAI's Grok-2 Vision model (version 1212) with image support and 32K context window", + }, } as const satisfies Record diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 8e84981d8a..bd925b0e90 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -14,6 +14,7 @@ import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRe import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" +import { MdmService } from "../services/mdm/MdmService" import { t } from "../i18n" /** @@ -226,7 +227,17 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit editor.viewColumn || 0)) // Check if there are any visible text editors, otherwise open a new group diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 9f29185eba..b4bcfa62d6 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -148,7 +148,9 @@ export class CustomModesManager { cleanedContent = this.cleanInvisibleCharacters(cleanedContent) try { - return yaml.parse(cleanedContent) + const parsed = yaml.parse(cleanedContent) + // Ensure we never return null or undefined + return parsed ?? {} } catch (yamlError) { // For .roomodes files, try JSON as fallback if (filePath.endsWith(ROOMODES_FILENAME)) { @@ -180,6 +182,12 @@ export class CustomModesManager { try { const content = await fs.readFile(filePath, "utf-8") const settings = this.parseYamlSafely(content, filePath) + + // Ensure settings has customModes property + if (!settings || typeof settings !== "object" || !settings.customModes) { + return [] + } + const result = customModesSettingsSchema.safeParse(settings) if (!result.success) { @@ -458,7 +466,15 @@ export class CustomModesManager { settings = { customModes: [] } } - settings.customModes = operation(settings.customModes || []) + // Ensure settings is an object and has customModes property + if (!settings || typeof settings !== "object") { + settings = { customModes: [] } + } + if (!settings.customModes) { + settings.customModes = [] + } + + settings.customModes = operation(settings.customModes) await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8") } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 04938e3625..0caa3fca47 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -58,7 +58,7 @@ "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}", "settings_import_failed": "Ha fallat la importació de la configuració: {{error}}.", "mistake_limit_guidance": "Això pot indicar un error en el procés de pensament del model o la incapacitat d'utilitzar una eina correctament, que es pot mitigar amb orientació de l'usuari (p. ex. \"Prova de dividir la tasca en passos més petits\").", - "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual infringeix la configuració de la teva organització", + "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual no és compatible amb la configuració de la teva organització", "condense_failed": "Ha fallat la condensació del context", "condense_not_enough_messages": "No hi ha prou missatges per condensar el context", "condensed_recently": "El context s'ha condensat recentment; s'omet aquest intent", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 9fa27c89c6..bf9af547ca 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}", "settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}.", "mistake_limit_guidance": "Dies kann auf einen Fehler im Denkprozess des Modells oder die Unfähigkeit hinweisen, ein Tool richtig zu verwenden, was durch Benutzerführung behoben werden kann (z.B. \"Versuche, die Aufgabe in kleinere Schritte zu unterteilen\").", - "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil verstößt gegen die Einstellungen deiner Organisation", + "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil ist nicht kompatibel mit den Einstellungen deiner Organisation", "condense_failed": "Fehler beim Verdichten des Kontexts", "condense_not_enough_messages": "Nicht genügend Nachrichten zum Verdichten des Kontexts", "condensed_recently": "Kontext wurde kürzlich verdichtet; dieser Versuch wird übersprungen", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index f907a5745b..3004038d42 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Cannot access path {{path}}: {{error}}", "settings_import_failed": "Settings import failed: {{error}}.", "mistake_limit_guidance": "This may indicate a failure in the model's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. \"Try breaking down the task into smaller steps\").", - "violated_organization_allowlist": "Failed to run task: the current profile violates your organization settings", + "violated_organization_allowlist": "Failed to run task: the current profile isn't compatible with your organization settings", "condense_failed": "Failed to condense context", "condense_not_enough_messages": "Not enough messages to condense context", "condensed_recently": "Context was condensed recently; skipping this attempt", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 68750e888f..1a16dbf1ae 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}", "settings_import_failed": "Error al importar la configuración: {{error}}.", "mistake_limit_guidance": "Esto puede indicar un fallo en el proceso de pensamiento del modelo o la incapacidad de usar una herramienta correctamente, lo cual puede mitigarse con orientación del usuario (ej. \"Intenta dividir la tarea en pasos más pequeños\").", - "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual infringe la configuración de tu organización", + "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual no es compatible con la configuración de tu organización", "condense_failed": "Error al condensar el contexto", "condense_not_enough_messages": "No hay suficientes mensajes para condensar el contexto", "condensed_recently": "El contexto se condensó recientemente; se omite este intento", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index e985fe9468..98945f305d 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}", "settings_import_failed": "Échec de l'importation des paramètres : {{error}}", "mistake_limit_guidance": "Cela peut indiquer un échec dans le processus de réflexion du modèle ou une incapacité à utiliser un outil correctement, ce qui peut être atténué avec des conseils de l'utilisateur (par ex. \"Essaie de diviser la tâche en étapes plus petites\").", - "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel enfreint les paramètres de votre organisation", + "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel n'est pas compatible avec les paramètres de votre organisation", "condense_failed": "Échec de la condensation du contexte", "condense_not_enough_messages": "Pas assez de messages pour condenser le contexte", "condensed_recently": "Le contexte a été condensé récemment ; cette tentative est ignorée", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index a16a22ece5..7daa0046ec 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}", "settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।", "mistake_limit_guidance": "यह मॉडल की सोच प्रक्रिया में विफलता या किसी टूल का सही उपयोग न कर पाने का संकेत हो सकता है, जिसे उपयोगकर्ता के मार्गदर्शन से ठीक किया जा सकता है (जैसे \"कार्य को छोटे चरणों में बांटने की कोशिश करें\")।", - "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स का उल्लंघन करती है", + "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है", "condense_failed": "संदर्भ को संक्षिप्त करने में विफल", "condense_not_enough_messages": "संदर्भ को संक्षिप्त करने के लिए पर्याप्त संदेश नहीं हैं", "condensed_recently": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index d40bce9186..c021dab4cd 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Tidak dapat mengakses path {{path}}: {{error}}", "settings_import_failed": "Impor pengaturan gagal: {{error}}.", "mistake_limit_guidance": "Ini mungkin menunjukkan kegagalan dalam proses pemikiran model atau ketidakmampuan untuk menggunakan tool dengan benar, yang dapat diatasi dengan beberapa panduan pengguna (misalnya \"Coba bagi tugas menjadi langkah-langkah yang lebih kecil\").", - "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini melanggar pengaturan organisasi kamu", + "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini tidak kompatibel dengan pengaturan organisasi kamu", "condense_failed": "Gagal mengompres konteks", "condense_not_enough_messages": "Tidak cukup pesan untuk mengompres konteks", "condensed_recently": "Konteks baru saja dikompres; melewati percobaan ini", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a6782267ce..ff45cd8f1e 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}", "settings_import_failed": "Importazione delle impostazioni fallita: {{error}}.", "mistake_limit_guidance": "Questo può indicare un fallimento nel processo di pensiero del modello o l'incapacità di utilizzare correttamente uno strumento, che può essere mitigato con la guida dell'utente (ad es. \"Prova a suddividere l'attività in passaggi più piccoli\").", - "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente viola le impostazioni della tua organizzazione", + "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente non è compatibile con le impostazioni della tua organizzazione", "condense_failed": "Impossibile condensare il contesto", "condense_not_enough_messages": "Non ci sono abbastanza messaggi per condensare il contesto", "condensed_recently": "Il contesto è stato condensato di recente; questo tentativo viene saltato", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 9722281d0c..4d9b88d114 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}", "settings_import_failed": "設定のインポートに失敗しました:{{error}}", "mistake_limit_guidance": "これは、モデルの思考プロセスの失敗やツールを適切に使用できないことを示している可能性があり、ユーザーのガイダンスによって軽減できます(例:「タスクをより小さなステップに分割してみてください」)。", - "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定に違反しています", + "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定と互換性がありません", "condense_failed": "コンテキストの圧縮に失敗しました", "condense_not_enough_messages": "コンテキストを圧縮するのに十分なメッセージがありません", "condensed_recently": "コンテキストは最近圧縮されました;この試行をスキップします", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 81c80154d0..34d9bced71 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}", "settings_import_failed": "설정 가져오기 실패: {{error}}.", "mistake_limit_guidance": "이는 모델의 사고 과정 실패나 도구를 제대로 사용하지 못하는 것을 나타낼 수 있으며, 사용자 가이드를 통해 완화할 수 있습니다 (예: \"작업을 더 작은 단계로 나누어 시도해보세요\").", - "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정을 위반합니다", + "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정과 호환되지 않습니다", "condense_failed": "컨텍스트 압축에 실패했습니다", "condense_not_enough_messages": "컨텍스트를 압축할 메시지가 충분하지 않습니다", "condensed_recently": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 68ad31e728..dff1bb83f7 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Kan pad {{path}} niet openen: {{error}}", "settings_import_failed": "Importeren van instellingen mislukt: {{error}}.", "mistake_limit_guidance": "Dit kan duiden op een fout in het denkproces van het model of het onvermogen om een tool correct te gebruiken, wat kan worden verminderd met gebruikersbegeleiding (bijv. \"Probeer de taak op te delen in kleinere stappen\").", - "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel schendt de instellingen van uw organisatie", + "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel is niet compatibel met de instellingen van uw organisatie", "condense_failed": "Comprimeren van context mislukt", "condense_not_enough_messages": "Niet genoeg berichten om context te comprimeren", "condensed_recently": "Context is recent gecomprimeerd; deze poging wordt overgeslagen", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 7dc510e71c..33c58b4752 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}", "settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}.", "mistake_limit_guidance": "To może wskazywać na błąd w procesie myślowym modelu lub niezdolność do prawidłowego użycia narzędzia, co można złagodzić poprzez wskazówki użytkownika (np. \"Spróbuj podzielić zadanie na mniejsze kroki\").", - "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil narusza ustawienia Twojej organizacji", + "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil nie jest kompatybilny z ustawieniami Twojej organizacji", "condense_failed": "Nie udało się skondensować kontekstu", "condense_not_enough_messages": "Za mało wiadomości do skondensowania kontekstu", "condensed_recently": "Kontekst został niedawno skondensowany; pomijanie tej próby", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 7ee3b8a658..ce9dc113e2 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -58,7 +58,7 @@ "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}", "settings_import_failed": "Falha ao importar configurações: {{error}}", "mistake_limit_guidance": "Isso pode indicar uma falha no processo de pensamento do modelo ou incapacidade de usar uma ferramenta adequadamente, o que pode ser mitigado com orientação do usuário (ex. \"Tente dividir a tarefa em etapas menores\").", - "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual viola as configurações da sua organização", + "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual não é compatível com as configurações da sua organização", "condense_failed": "Falha ao condensar o contexto", "condense_not_enough_messages": "Não há mensagens suficientes para condensar o contexto", "condensed_recently": "O contexto foi condensado recentemente; pulando esta tentativa", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 8e100bff9a..a3b6c322b2 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Невозможно получить доступ к пути {{path}}: {{error}}", "settings_import_failed": "Не удалось импортировать настройки: {{error}}.", "mistake_limit_guidance": "Это может указывать на сбой в процессе мышления модели или неспособность правильно использовать инструмент, что можно смягчить с помощью руководства пользователя (например, \"Попробуйте разбить задачу на более мелкие шаги\").", - "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль нарушает настройки вашей организации", + "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль несовместим с настройками вашей организации", "condense_failed": "Не удалось сжать контекст", "condense_not_enough_messages": "Недостаточно сообщений для сжатия контекста", "condensed_recently": "Контекст был недавно сжат; пропускаем эту попытку", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index df8212cf3c..042fa88d15 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}", "settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}.", "mistake_limit_guidance": "Bu, modelin düşünce sürecindeki bir başarısızlığı veya bir aracı düzgün kullanamama durumunu gösterebilir, bu da kullanıcı rehberliği ile hafifletilebilir (örn. \"Görevi daha küçük adımlara bölmeyi deneyin\").", - "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarını ihlal ediyor", + "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarıyla uyumlu değil", "condense_failed": "Bağlam sıkıştırılamadı", "condense_not_enough_messages": "Bağlamı sıkıştırmak için yeterli mesaj yok", "condensed_recently": "Bağlam yakın zamanda sıkıştırıldı; bu deneme atlanıyor", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 6710cd136d..183ae7b41a 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}", "settings_import_failed": "Nhập cài đặt thất bại: {{error}}.", "mistake_limit_guidance": "Điều này có thể cho thấy sự thất bại trong quá trình suy nghĩ của mô hình hoặc không thể sử dụng công cụ đúng cách, có thể được giảm thiểu bằng hướng dẫn của người dùng (ví dụ: \"Hãy thử chia nhỏ nhiệm vụ thành các bước nhỏ hơn\").", - "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại vi phạm cài đặt của tổ chức của bạn", + "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại không tương thích với cài đặt của tổ chức của bạn", "condense_failed": "Không thể nén ngữ cảnh", "condense_not_enough_messages": "Không đủ tin nhắn để nén ngữ cảnh", "condensed_recently": "Ngữ cảnh đã được nén gần đây; bỏ qua lần thử này", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 5a8fc51ef1..a45efa4b1c 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -59,7 +59,7 @@ "cannot_access_path": "无法访问路径 {{path}}:{{error}}", "settings_import_failed": "设置导入失败:{{error}}。", "mistake_limit_guidance": "这可能表明模型思维过程失败或无法正确使用工具,可通过用户指导来缓解(例如\"尝试将任务分解为更小的步骤\")。", - "violated_organization_allowlist": "执行任务失败:当前配置文件违反了您的组织设置", + "violated_organization_allowlist": "执行任务失败:当前配置文件与您的组织设置不兼容", "condense_failed": "压缩上下文失败", "condense_not_enough_messages": "没有足够的对话来压缩上下文", "condensed_recently": "上下文最近已压缩;跳过此次尝试", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 2bfad5e00f..3fbbc050f4 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -54,7 +54,7 @@ "cannot_access_path": "無法存取路徑 {{path}}:{{error}}", "settings_import_failed": "設定匯入失敗:{{error}}。", "mistake_limit_guidance": "這可能表明模型思維過程失敗或無法正確使用工具,可透過使用者指導來緩解(例如「嘗試將工作分解為更小的步驟」)。", - "violated_organization_allowlist": "執行工作失敗:目前設定檔違反了您的組織設定", + "violated_organization_allowlist": "執行工作失敗:目前設定檔與您的組織設定不相容", "condense_failed": "壓縮上下文失敗", "condense_not_enough_messages": "沒有足夠的訊息來壓縮上下文", "condensed_recently": "上下文最近已壓縮;跳過此次嘗試", diff --git a/src/integrations/misc/__tests__/open-file.spec.ts b/src/integrations/misc/__tests__/open-file.spec.ts new file mode 100644 index 0000000000..e8f9be259d --- /dev/null +++ b/src/integrations/misc/__tests__/open-file.spec.ts @@ -0,0 +1,240 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as vscode from "vscode" +import * as path from "path" +import * as os from "os" +import { openFile } from "../open-file" + +// Mock vscode module +vi.mock("vscode", () => ({ + Uri: { + file: vi.fn((path: string) => ({ fsPath: path })), + }, + workspace: { + fs: { + stat: vi.fn(), + writeFile: vi.fn(), + }, + openTextDocument: vi.fn(), + }, + window: { + showTextDocument: vi.fn(), + showErrorMessage: vi.fn(), + tabGroups: { + all: [], + }, + activeTextEditor: undefined, + }, + commands: { + executeCommand: vi.fn(), + }, + FileType: { + Directory: 2, + File: 1, + }, + Selection: vi.fn((startLine: number, startChar: number, endLine: number, endChar: number) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + TabInputText: vi.fn(), +})) + +// Mock utils +vi.mock("../../utils/path", () => { + const nodePath = require("path") + return { + arePathsEqual: vi.fn((a: string, b: string) => a === b), + getWorkspacePath: vi.fn(() => { + // In tests, we need to return a consistent workspace path + // The actual workspace is /Users/roocode/rc2 in local, but varies in CI + const cwd = process.cwd() + // If we're in the src directory, go up one level to get workspace root + if (cwd.endsWith("/src")) { + return nodePath.dirname(cwd) + } + return cwd + }), + } +}) + +// Mock i18n +vi.mock("../../i18n", () => ({ + t: vi.fn((key: string, params?: any) => { + // Return the key without namespace prefix to match actual behavior + if (key.startsWith("common:")) { + return key.replace("common:", "") + } + return key + }), +})) + +describe("openFile", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(console, "warn").mockImplementation(() => {}) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("decodeURIComponent error handling", () => { + it("should handle invalid URI encoding gracefully", async () => { + const invalidPath = "test%ZZinvalid.txt" // Invalid percent encoding + const mockDocument = { uri: { fsPath: invalidPath } } + + vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ + type: vscode.FileType.File, + ctime: 0, + mtime: 0, + size: 0, + }) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + + await openFile(invalidPath) + + // Should log a warning about decode failure + expect(console.warn).toHaveBeenCalledWith( + "Failed to decode file path: URIError: URI malformed. Using original path.", + ) + + // Should still attempt to open the file with the original path + expect(vscode.workspace.openTextDocument).toHaveBeenCalled() + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + it("should successfully decode valid URI-encoded paths", async () => { + const encodedPath = "./%5Btest%5D/file.txt" // [test] encoded + const decodedPath = "./[test]/file.txt" + const mockDocument = { uri: { fsPath: decodedPath } } + + vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ + type: vscode.FileType.File, + ctime: 0, + mtime: 0, + size: 0, + }) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + + await openFile(encodedPath) + + // Should not log any warnings + expect(console.warn).not.toHaveBeenCalled() + + // Should use the decoded path - verify it contains the decoded brackets + // On Windows, the path will include backslashes instead of forward slashes + const expectedPathSegment = process.platform === "win32" ? "[test]\\file.txt" : "[test]/file.txt" + expect(vscode.Uri.file).toHaveBeenCalledWith(expect.stringContaining(expectedPathSegment)) + expect(vscode.workspace.openTextDocument).toHaveBeenCalled() + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + it("should handle paths with special characters that need encoding", async () => { + const pathWithSpecialChars = "./[brackets]/file with spaces.txt" + const mockDocument = { uri: { fsPath: pathWithSpecialChars } } + + vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ + type: vscode.FileType.File, + ctime: 0, + mtime: 0, + size: 0, + }) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + + await openFile(pathWithSpecialChars) + + // Should work without errors + expect(console.warn).not.toHaveBeenCalled() + expect(vscode.workspace.openTextDocument).toHaveBeenCalled() + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + it("should handle already decoded paths without double-decoding", async () => { + const normalPath = "./normal/file.txt" + const mockDocument = { uri: { fsPath: normalPath } } + + vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ + type: vscode.FileType.File, + ctime: 0, + mtime: 0, + size: 0, + }) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + + await openFile(normalPath) + + // Should work without errors + expect(console.warn).not.toHaveBeenCalled() + expect(vscode.workspace.openTextDocument).toHaveBeenCalled() + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + }) + + describe("error handling", () => { + it("should show error message when file does not exist", async () => { + const nonExistentPath = "./does/not/exist.txt" + + vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found")) + + await openFile(nonExistentPath) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file") + }) + + it("should handle generic errors", async () => { + const testPath = "./test.txt" + + vi.mocked(vscode.workspace.fs.stat).mockRejectedValue("Not an Error object") + + await openFile(testPath) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file") + }) + }) + + describe("directory handling", () => { + it("should reveal directories in explorer", async () => { + const dirPath = "./components" + + vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ + type: vscode.FileType.Directory, + ctime: 0, + mtime: 0, + size: 0, + }) + + await openFile(dirPath) + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + "revealInExplorer", + expect.objectContaining({ fsPath: expect.stringContaining("components") }), + ) + expect(vscode.commands.executeCommand).toHaveBeenCalledWith("list.expand") + expect(vscode.workspace.openTextDocument).not.toHaveBeenCalled() + }) + }) + + describe("file creation", () => { + it("should create new files when create option is true", async () => { + const newFilePath = "./new/file.txt" + const content = "Hello, world!" + + vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found")) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + + await openFile(newFilePath, { create: true, content }) + + // On Windows, the path will include backslashes instead of forward slashes + const expectedPathSegment = process.platform === "win32" ? "new\\file.txt" : "new/file.txt" + expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith( + expect.objectContaining({ fsPath: expect.stringContaining(expectedPathSegment) }), + Buffer.from(content, "utf8"), + ) + expect(vscode.workspace.openTextDocument).toHaveBeenCalled() + }) + }) +}) diff --git a/src/integrations/misc/open-file.ts b/src/integrations/misc/open-file.ts index f05c10dc96..a9ab44f7e5 100644 --- a/src/integrations/misc/open-file.ts +++ b/src/integrations/misc/open-file.ts @@ -12,9 +12,19 @@ interface OpenFileOptions { export async function openFile(filePath: string, options: OpenFileOptions = {}) { try { + // Store the original path for error messages before any modifications + const originalFilePathForError = filePath + + // Try to decode the URI component, but if it fails, use the original path + try { + filePath = decodeURIComponent(filePath) + } catch (decodeError) { + // If decoding fails (e.g., invalid escape sequences), continue with the original path + console.warn(`Failed to decode file path: ${decodeError}. Using original path.`) + } + const workspaceRoot = getWorkspacePath() const homeDir = os.homedir() - const originalFilePathForError = filePath // Keep original for error messages const attemptPaths: string[] = [] diff --git a/src/package.json b/src/package.json index 58c62c207f..14f6607742 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.23.3", + "version": "3.23.6", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -75,6 +75,11 @@ "title": "%command.newTask.title%", "icon": "$(add)" }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "%command.prompts.title%", + "icon": "$(organization)" + }, { "command": "roo-cline.mcpButtonClicked", "title": "%command.mcpServers.title%", @@ -219,33 +224,38 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.mcpButtonClicked", + "command": "roo-cline.settingsButtonClicked", "group": "navigation@2", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.accountButtonClicked", "group": "navigation@3", "when": "view == roo-cline.SidebarProvider" }, { "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", + "group": "overflow@1", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.marketplaceButtonClicked", + "group": "overflow@2", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.promptsButtonClicked", + "group": "overflow@3", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "overflow@4", "when": "view == roo-cline.SidebarProvider" }, { "command": "roo-cline.popoutButtonClicked", - "group": "navigation@5", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.accountButtonClicked", - "group": "navigation@6", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@7", + "group": "overflow@5", "when": "view == roo-cline.SidebarProvider" } ], @@ -256,28 +266,38 @@ "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.mcpButtonClicked", + "command": "roo-cline.settingsButtonClicked", "group": "navigation@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.accountButtonClicked", "group": "navigation@3", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", + "group": "overflow@1", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.accountButtonClicked", - "group": "navigation@5", + "command": "roo-cline.marketplaceButtonClicked", + "group": "overflow@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@6", + "command": "roo-cline.promptsButtonClicked", + "group": "overflow@3", + "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "overflow@4", + "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "overflow@5", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } ] diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 7002283226..bd782da84c 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -12,6 +12,7 @@ import { CacheManager } from "./cache-manager" import fs from "fs/promises" import ignore from "ignore" import path from "path" +import { t } from "../../i18n" export class CodeIndexManager { // --- Singleton Implementation --- @@ -261,12 +262,16 @@ export class CodeIndexManager { // Validate embedder configuration before proceeding const validationResult = await this._serviceFactory.validateEmbedder(embedder) if (!validationResult.valid) { - // Set error state with clear message - this._stateManager.setSystemState( - "Error", - validationResult.error || "Embedder configuration validation failed", - ) - throw new Error(validationResult.error || "Invalid embedder configuration") + const errorMessage = validationResult.error || "Embedder configuration validation failed" + // Always attempt translation, use original as fallback + let translatedMessage = t(errorMessage) + // If translation returns a different value (stripped namespace), use original + if (translatedMessage !== errorMessage && !translatedMessage.includes(":")) { + translatedMessage = errorMessage + } + + this._stateManager.setSystemState("Error", translatedMessage) + throw new Error(translatedMessage) } // (Re)Initialize orchestrator diff --git a/src/services/glob/__tests__/gitignore-integration.spec.ts b/src/services/glob/__tests__/gitignore-integration.spec.ts new file mode 100644 index 0000000000..1361816b17 --- /dev/null +++ b/src/services/glob/__tests__/gitignore-integration.spec.ts @@ -0,0 +1,209 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest" +import * as path from "path" +import * as fs from "fs" +import * as os from "os" + +// Mock ripgrep to avoid filesystem dependencies +vi.mock("../../ripgrep", () => ({ + getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"), +})) + +// Mock vscode +vi.mock("vscode", () => ({ + env: { + appRoot: "/mock/app/root", + }, +})) + +// Mock child_process to simulate ripgrep behavior +vi.mock("child_process", () => ({ + spawn: vi.fn(), +})) + +vi.mock("../../path", () => ({ + arePathsEqual: vi.fn().mockReturnValue(false), +})) + +import { listFiles } from "../list-files" +import * as childProcess from "child_process" + +describe("list-files gitignore integration", () => { + let tempDir: string + let originalCwd: string + + beforeEach(async () => { + vi.clearAllMocks() + + // Create a temporary directory for testing + tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "roo-gitignore-test-")) + originalCwd = process.cwd() + }) + + afterEach(async () => { + process.chdir(originalCwd) + // Clean up temp directory + await fs.promises.rm(tempDir, { recursive: true, force: true }) + }) + + it("should properly filter directories based on .gitignore patterns", async () => { + // Setup test directory structure + await fs.promises.mkdir(path.join(tempDir, "src")) + await fs.promises.mkdir(path.join(tempDir, "node_modules")) + await fs.promises.mkdir(path.join(tempDir, "build")) + await fs.promises.mkdir(path.join(tempDir, "dist")) + await fs.promises.mkdir(path.join(tempDir, "allowed-dir")) + + // Create .gitignore file + await fs.promises.writeFile(path.join(tempDir, ".gitignore"), "node_modules/\nbuild/\ndist/\n*.log\n") + + // Create some files + await fs.promises.writeFile(path.join(tempDir, "src", "index.ts"), "console.log('hello')") + await fs.promises.writeFile(path.join(tempDir, "allowed-dir", "file.txt"), "content") + + // Mock ripgrep to return files that would not be gitignored + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // Simulate ripgrep output (files that are not gitignored) + const files = + [path.join(tempDir, "src", "index.ts"), path.join(tempDir, "allowed-dir", "file.txt")].join( + "\n", + ) + "\n" + setTimeout(() => callback(files), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles in recursive mode + const [files, didHitLimit] = await listFiles(tempDir, true, 100) + + // Filter out only directories from the results + const directoriesInResult = files.filter((f) => f.endsWith("/")) + + // Verify that gitignored directories are NOT included + expect(directoriesInResult).not.toContain(path.join(tempDir, "node_modules") + "/") + expect(directoriesInResult).not.toContain(path.join(tempDir, "build") + "/") + expect(directoriesInResult).not.toContain(path.join(tempDir, "dist") + "/") + + // Verify that allowed directories ARE included + expect(directoriesInResult).toContain(path.join(tempDir, "src") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "allowed-dir") + "/") + }) + + it("should handle nested .gitignore files correctly", async () => { + // Setup nested directory structure + await fs.promises.mkdir(path.join(tempDir, "src"), { recursive: true }) + await fs.promises.mkdir(path.join(tempDir, "src", "components")) + await fs.promises.mkdir(path.join(tempDir, "src", "temp")) + await fs.promises.mkdir(path.join(tempDir, "src", "utils")) + + // Create root .gitignore + await fs.promises.writeFile(path.join(tempDir, ".gitignore"), "node_modules/\n") + + // Create nested .gitignore in src/ + await fs.promises.writeFile(path.join(tempDir, "src", ".gitignore"), "temp/\n") + + // Mock ripgrep + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback(""), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles in recursive mode + const [files, didHitLimit] = await listFiles(tempDir, true, 100) + + // Filter out only directories from the results + const directoriesInResult = files.filter((f) => f.endsWith("/")) + + // Verify that nested gitignored directories are NOT included + expect(directoriesInResult).not.toContain(path.join(tempDir, "src", "temp") + "/") + + // Verify that allowed directories ARE included + expect(directoriesInResult).toContain(path.join(tempDir, "src") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "src", "components") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "src", "utils") + "/") + }) + + it("should respect .gitignore in non-recursive mode too", async () => { + // Setup test directory structure + await fs.promises.mkdir(path.join(tempDir, "src")) + await fs.promises.mkdir(path.join(tempDir, "node_modules")) + await fs.promises.mkdir(path.join(tempDir, "allowed-dir")) + + // Create .gitignore file + await fs.promises.writeFile(path.join(tempDir, ".gitignore"), "node_modules/\n") + + // Mock ripgrep for non-recursive mode + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // In non-recursive mode, ripgrep should now respect .gitignore + const files = [path.join(tempDir, "src"), path.join(tempDir, "allowed-dir")].join("\n") + "\n" + setTimeout(() => callback(files), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles in NON-recursive mode + const [files, didHitLimit] = await listFiles(tempDir, false, 100) + + // Verify ripgrep was called without --no-ignore-vcs (should respect .gitignore) + const [rgPath, args] = mockSpawn.mock.calls[0] + expect(args).not.toContain("--no-ignore-vcs") + + // Filter out only directories from the results + const directoriesInResult = files.filter((f) => f.endsWith("/")) + + // Verify that gitignored directories are NOT included even in non-recursive mode + expect(directoriesInResult).not.toContain(path.join(tempDir, "node_modules") + "/") + + // Verify that allowed directories ARE included + expect(directoriesInResult).toContain(path.join(tempDir, "src") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "allowed-dir") + "/") + }) +}) diff --git a/src/services/glob/__tests__/gitignore-test.spec.ts b/src/services/glob/__tests__/gitignore-test.spec.ts new file mode 100644 index 0000000000..cef884b584 --- /dev/null +++ b/src/services/glob/__tests__/gitignore-test.spec.ts @@ -0,0 +1,147 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest" +import * as path from "path" +import * as fs from "fs" +import * as os from "os" + +// Mock ripgrep to avoid filesystem dependencies +vi.mock("../../ripgrep", () => ({ + getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"), +})) + +// Mock vscode +vi.mock("vscode", () => ({ + env: { + appRoot: "/mock/app/root", + }, +})) + +vi.mock("child_process", () => ({ + spawn: vi.fn(), +})) + +vi.mock("../../path", () => ({ + arePathsEqual: vi.fn().mockReturnValue(false), +})) + +import { listFiles } from "../list-files" +import * as childProcess from "child_process" + +describe("list-files gitignore support", () => { + let tempDir: string + let originalCwd: string + + beforeEach(async () => { + vi.clearAllMocks() + + // Create a temporary directory for testing + tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "roo-test-")) + originalCwd = process.cwd() + process.chdir(tempDir) + }) + + afterEach(async () => { + process.chdir(originalCwd) + // Clean up temp directory + await fs.promises.rm(tempDir, { recursive: true, force: true }) + }) + + it("should respect .gitignore patterns for directories in recursive mode", async () => { + // Setup test directory structure + await fs.promises.mkdir(path.join(tempDir, "src")) + await fs.promises.mkdir(path.join(tempDir, "node_modules")) + await fs.promises.mkdir(path.join(tempDir, "build")) + await fs.promises.mkdir(path.join(tempDir, "ignored-dir")) + + // Create .gitignore file + await fs.promises.writeFile(path.join(tempDir, ".gitignore"), "node_modules/\nbuild/\nignored-dir/\n") + + // Create some files + await fs.promises.writeFile(path.join(tempDir, "src", "index.ts"), "") + await fs.promises.writeFile(path.join(tempDir, "node_modules", "package.json"), "") + await fs.promises.writeFile(path.join(tempDir, "build", "output.js"), "") + await fs.promises.writeFile(path.join(tempDir, "ignored-dir", "file.txt"), "") + + // Mock ripgrep to return only non-ignored files + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // Ripgrep should respect .gitignore and only return src/index.ts + setTimeout(() => callback(`${path.join(tempDir, "src", "index.ts")}\n`), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles in recursive mode + const [files, didHitLimit] = await listFiles(tempDir, true, 100) + + // Verify that gitignored directories are not included + const directoriesInResult = files.filter((f) => f.endsWith("/")) + + expect(directoriesInResult).not.toContain(path.join(tempDir, "node_modules") + "/") + expect(directoriesInResult).not.toContain(path.join(tempDir, "build") + "/") + expect(directoriesInResult).not.toContain(path.join(tempDir, "ignored-dir") + "/") + + // But src/ should be included + expect(directoriesInResult).toContain(path.join(tempDir, "src") + "/") + }) + + it("should handle nested .gitignore files", async () => { + // Setup nested directory structure + await fs.promises.mkdir(path.join(tempDir, "src"), { recursive: true }) + await fs.promises.mkdir(path.join(tempDir, "src", "components")) + await fs.promises.mkdir(path.join(tempDir, "src", "temp")) + + // Create root .gitignore + await fs.promises.writeFile(path.join(tempDir, ".gitignore"), "node_modules/\n") + + // Create nested .gitignore in src/ + await fs.promises.writeFile(path.join(tempDir, "src", ".gitignore"), "temp/\n") + + // Mock ripgrep + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback(""), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles in recursive mode + const [files, didHitLimit] = await listFiles(tempDir, true, 100) + + // Verify that nested gitignored directories are not included + const directoriesInResult = files.filter((f) => f.endsWith("/")) + + expect(directoriesInResult).not.toContain(path.join(tempDir, "src", "temp") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "src") + "/") + expect(directoriesInResult).toContain(path.join(tempDir, "src", "components") + "/") + }) +}) diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 3fb1b2e154..3164ed1eb0 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -3,6 +3,7 @@ import * as path from "path" import * as fs from "fs" import * as childProcess from "child_process" import * as vscode from "vscode" +import ignore from "ignore" import { arePathsEqual } from "../../utils/path" import { getBinPath } from "../../services/ripgrep" import { DIRS_TO_IGNORE } from "./constants" @@ -34,9 +35,9 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb // Get files using ripgrep const files = await listFilesWithRipgrep(rgPath, dirPath, recursive, limit) - // Get directories with proper filtering - const gitignorePatterns = await parseGitignoreFile(dirPath, recursive) - const directories = await listFilteredDirectories(dirPath, recursive, gitignorePatterns) + // Get directories with proper filtering using ignore library + const ignoreInstance = await createIgnoreInstance(dirPath) + const directories = await listFilteredDirectories(dirPath, recursive, ignoreInstance) // Combine and format the results return formatAndCombineResults(files, directories, limit) @@ -134,8 +135,8 @@ function buildNonRecursiveArgs(): string[] { args.push("-g", "*") args.push("--maxdepth", "1") // ripgrep uses maxdepth, not max-depth - // Don't respect .gitignore in non-recursive mode (consistent with original behavior) - args.push("--no-ignore-vcs") + // Respect .gitignore in non-recursive mode too + // (ripgrep respects .gitignore by default) // Apply directory exclusions for non-recursive searches for (const dir of DIRS_TO_IGNORE) { @@ -153,37 +154,61 @@ function buildNonRecursiveArgs(): string[] { } /** - * Parse the .gitignore file if it exists and is relevant + * Create an ignore instance that handles .gitignore files properly + * This replaces the custom gitignore parsing with the proper ignore library */ -async function parseGitignoreFile(dirPath: string, recursive: boolean): Promise { - if (!recursive) { - return [] // Only needed for recursive mode +async function createIgnoreInstance(dirPath: string): Promise> { + const ignoreInstance = ignore() + const absolutePath = path.resolve(dirPath) + + // Find all .gitignore files from the target directory up to the root + const gitignoreFiles = await findGitignoreFiles(absolutePath) + + // Add patterns from all .gitignore files + for (const gitignoreFile of gitignoreFiles) { + try { + const content = await fs.promises.readFile(gitignoreFile, "utf8") + ignoreInstance.add(content) + } catch (err) { + // Continue if we can't read a .gitignore file + console.warn(`Error reading .gitignore at ${gitignoreFile}: ${err}`) + } } - const absolutePath = path.resolve(dirPath) - const gitignorePath = path.join(absolutePath, ".gitignore") + // Always ignore .gitignore files themselves + ignoreInstance.add(".gitignore") - try { - // Check if .gitignore exists - const exists = await fs.promises - .access(gitignorePath) - .then(() => true) - .catch(() => false) + return ignoreInstance +} - if (!exists) { - return [] +/** + * Find all .gitignore files from the given directory up to the workspace root + */ +async function findGitignoreFiles(startPath: string): Promise { + const gitignoreFiles: string[] = [] + let currentPath = startPath + + // Walk up the directory tree looking for .gitignore files + while (currentPath && currentPath !== path.dirname(currentPath)) { + const gitignorePath = path.join(currentPath, ".gitignore") + + try { + await fs.promises.access(gitignorePath) + gitignoreFiles.push(gitignorePath) + } catch { + // .gitignore doesn't exist at this level, continue } - // Read and parse .gitignore file - const content = await fs.promises.readFile(gitignorePath, "utf8") - return content - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")) - } catch (err) { - console.warn(`Error reading .gitignore: ${err}`) - return [] // Continue without gitignore patterns on error + // Move up one directory + const parentPath = path.dirname(currentPath) + if (parentPath === currentPath) { + break // Reached root + } + currentPath = parentPath } + + // Return in reverse order (root .gitignore first, then more specific ones) + return gitignoreFiles.reverse() } /** @@ -192,7 +217,7 @@ async function parseGitignoreFile(dirPath: string, recursive: boolean): Promise< async function listFilteredDirectories( dirPath: string, recursive: boolean, - gitignorePatterns: string[], + ignoreInstance: ReturnType, ): Promise { const absolutePath = path.resolve(dirPath) const directories: string[] = [] @@ -209,7 +234,7 @@ async function listFilteredDirectories( const fullDirPath = path.join(currentPath, dirName) // Check if this directory should be included - if (shouldIncludeDirectory(dirName, recursive, gitignorePatterns)) { + if (shouldIncludeDirectory(dirName, fullDirPath, dirPath, ignoreInstance)) { // Add the directory to our results (with trailing slash) const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/` directories.push(formattedPath) @@ -236,7 +261,12 @@ async function listFilteredDirectories( /** * Determine if a directory should be included in results based on filters */ -function shouldIncludeDirectory(dirName: string, recursive: boolean, gitignorePatterns: string[]): boolean { +function shouldIncludeDirectory( + dirName: string, + fullDirPath: string, + basePath: string, + ignoreInstance: ReturnType, +): boolean { // Skip hidden directories if configured to ignore them if (dirName.startsWith(".") && DIRS_TO_IGNORE.includes(".*")) { return false @@ -247,8 +277,13 @@ function shouldIncludeDirectory(dirName: string, recursive: boolean, gitignorePa return false } - // Check against gitignore patterns in recursive mode - if (recursive && gitignorePatterns.length > 0 && isIgnoredByGitignore(dirName, gitignorePatterns)) { + // Check against gitignore patterns using the ignore library + // Calculate relative path from the base directory + const relativePath = path.relative(basePath, fullDirPath) + const normalizedPath = relativePath.replace(/\\/g, "/") + + // Check if the directory is ignored by .gitignore + if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) { return false } @@ -277,37 +312,6 @@ function isDirectoryExplicitlyIgnored(dirName: string): boolean { return false } -/** - * Check if a directory matches any gitignore patterns - */ -function isIgnoredByGitignore(dirName: string, gitignorePatterns: string[]): boolean { - for (const pattern of gitignorePatterns) { - // Directory patterns (ending with /) - if (pattern.endsWith("/")) { - const dirPattern = pattern.slice(0, -1) - if (dirName === dirPattern) { - return true - } - if (pattern.startsWith("**/") && dirName === dirPattern.slice(3)) { - return true - } - } - // Simple name patterns - else if (dirName === pattern) { - return true - } - // Wildcard patterns - else if (pattern.includes("*")) { - const regexPattern = pattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*") - const regex = new RegExp(`^${regexPattern}$`) - if (regex.test(dirName)) { - return true - } - } - } - - return false -} /** * Combine file and directory results and format them properly diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index 91f5bcad26..2274b65343 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -47,7 +47,9 @@ export class SimpleInstaller { let existingData: any = { customModes: [] } try { const existing = await fs.readFile(filePath, "utf-8") - existingData = yaml.parse(existing) || { customModes: [] } + const parsed = yaml.parse(existing) + // Ensure we have a valid object with customModes array + existingData = parsed && typeof parsed === "object" ? parsed : { customModes: [] } } catch (error: any) { if (error.code === "ENOENT") { // File doesn't exist, use default structure - this is fine @@ -253,7 +255,9 @@ export class SimpleInstaller { let existingData: any try { - existingData = yaml.parse(existing) + const parsed = yaml.parse(existing) + // Ensure we have a valid object + existingData = parsed && typeof parsed === "object" ? parsed : {} } catch (parseError) { // If we can't parse the file, we can't safely remove a mode const fileName = target === "project" ? ".roomodes" : "custom-modes.yaml" @@ -263,27 +267,30 @@ export class SimpleInstaller { ) } - if (existingData?.customModes) { - // Parse the item content to get the slug - let content: string - if (Array.isArray(item.content)) { - // Array of McpInstallationMethod objects - use first method - content = item.content[0].content - } else { - content = item.content - } - const modeData = yaml.parse(content || "") - - if (!modeData.slug) { - return // Nothing to remove if no slug - } - - // Remove mode with matching slug - existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug) - - // Always write back the file, even if empty - await fs.writeFile(filePath, yaml.stringify(existingData, { lineWidth: 0 }), "utf-8") + // Ensure customModes array exists + if (!existingData.customModes) { + existingData.customModes = [] } + + // Parse the item content to get the slug + let content: string + if (Array.isArray(item.content)) { + // Array of McpInstallationMethod objects - use first method + content = item.content[0].content + } else { + content = item.content + } + const modeData = yaml.parse(content || "") + + if (!modeData.slug) { + return // Nothing to remove if no slug + } + + // Remove mode with matching slug + existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug) + + // Always write back the file, even if empty + await fs.writeFile(filePath, yaml.stringify(existingData, { lineWidth: 0 }), "utf-8") } catch (error: any) { if (error.code === "ENOENT") { // File doesn't exist, nothing to remove diff --git a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts index 4934d0a6bc..546eb16f9a 100644 --- a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts +++ b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts @@ -89,6 +89,59 @@ describe("SimpleInstaller", () => { expect(writtenData.customModes.find((m: any) => m.slug === "test")).toBeDefined() }) + it("should handle empty .roomodes file", async () => { + // Empty file content + mockFs.readFile.mockResolvedValueOnce("") + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + const result = await installer.installItem(mockModeItem, { target: "project" }) + + expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes")) + expect(mockFs.writeFile).toHaveBeenCalled() + + // Verify the written content contains the new mode + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toHaveLength(1) + expect(writtenData.customModes[0].slug).toBe("test") + }) + + it("should handle .roomodes file with null content", async () => { + // File exists but yaml.parse returns null + mockFs.readFile.mockResolvedValueOnce("---\n") + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + const result = await installer.installItem(mockModeItem, { target: "project" }) + + expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes")) + expect(mockFs.writeFile).toHaveBeenCalled() + + // Verify the written content contains the new mode + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toHaveLength(1) + expect(writtenData.customModes[0].slug).toBe("test") + }) + + it("should handle .roomodes file without customModes property", async () => { + // File has valid YAML but no customModes property + const contentWithoutCustomModes = yaml.stringify({ someOtherProperty: "value" }) + mockFs.readFile.mockResolvedValueOnce(contentWithoutCustomModes) + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + const result = await installer.installItem(mockModeItem, { target: "project" }) + + expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes")) + expect(mockFs.writeFile).toHaveBeenCalled() + + // Verify the written content contains the new mode and preserves other properties + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toHaveLength(1) + expect(writtenData.customModes[0].slug).toBe("test") + expect(writtenData.someOtherProperty).toBe("value") + }) + it("should throw error when .roomodes contains invalid YAML", async () => { const invalidYaml = "invalid: yaml: content: {" @@ -224,5 +277,52 @@ describe("SimpleInstaller", () => { expect(mockFs.writeFile).not.toHaveBeenCalled() }) + + it("should handle empty .roomodes file during removal", async () => { + // Empty file content + mockFs.readFile.mockResolvedValueOnce("") + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + // Should not throw + await installer.removeItem(mockModeItem, { target: "project" }) + + // Should write back a valid structure with empty customModes + expect(mockFs.writeFile).toHaveBeenCalled() + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toEqual([]) + }) + + it("should handle .roomodes file with null content during removal", async () => { + // File exists but yaml.parse returns null + mockFs.readFile.mockResolvedValueOnce("---\n") + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + // Should not throw + await installer.removeItem(mockModeItem, { target: "project" }) + + // Should write back a valid structure with empty customModes + expect(mockFs.writeFile).toHaveBeenCalled() + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toEqual([]) + }) + + it("should handle .roomodes file without customModes property during removal", async () => { + // File has valid YAML but no customModes property + const contentWithoutCustomModes = yaml.stringify({ someOtherProperty: "value" }) + mockFs.readFile.mockResolvedValueOnce(contentWithoutCustomModes) + mockFs.writeFile.mockResolvedValueOnce(undefined as any) + + // Should not throw + await installer.removeItem(mockModeItem, { target: "project" }) + + // Should write back the file with the same content (no modes to remove) + expect(mockFs.writeFile).toHaveBeenCalled() + const writtenContent = mockFs.writeFile.mock.calls[0][1] as string + const writtenData = yaml.parse(writtenContent) + expect(writtenData.customModes).toEqual([]) + expect(writtenData.someOtherProperty).toBe("value") + }) }) }) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index b522b43ecc..ee622239e2 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -25,9 +25,8 @@ import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Pin, Check } from "lucide-react" -import { IconButton } from "./IconButton" -import { IndexingStatusDot } from "./IndexingStatusBadge" +import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react" +import { IndexingStatusBadge } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" @@ -952,34 +951,63 @@ const ChatTextArea = forwardRef( /> {isTtsPlaying && ( - + + + )}
- + + +
- + + +
{!inputValue && ( @@ -1145,14 +1173,29 @@ const ChatTextArea = forwardRef(
- - + + + +
diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index f3c268c2ba..ff5a0171b5 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useMemo } from "react" +import { Database } from "lucide-react" import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@/i18n/TranslationContext" @@ -6,11 +7,11 @@ import { useTooltip } from "@/hooks/useTooltip" import { CodeIndexPopover } from "./CodeIndexPopover" import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage" -interface IndexingStatusDotProps { +interface IndexingStatusBadgeProps { className?: string } -export const IndexingStatusDot: React.FC = ({ className }) => { +export const IndexingStatusBadge: React.FC = ({ className }) => { const { t } = useAppTranslation() const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 }) const [isHovered, setIsHovered] = useState(false) @@ -77,23 +78,23 @@ export const IndexingStatusDot: React.FC = ({ className handleMouseLeave() } - // Get status color classes based on status and hover state + // Get status color classes for the badge dot const getStatusColorClass = () => { const statusColors = { Standby: { - default: "bg-vscode-descriptionForeground/40", - hover: "bg-vscode-descriptionForeground/60", + default: "bg-vscode-descriptionForeground/60", + hover: "bg-vscode-descriptionForeground/80", }, Indexing: { - default: "bg-yellow-500/40 animate-pulse", + default: "bg-yellow-500 animate-pulse", hover: "bg-yellow-500 animate-pulse", }, Indexed: { - default: "bg-green-500/40", + default: "bg-green-500", hover: "bg-green-500", }, Error: { - default: "bg-red-500/40", + default: "bg-red-500", hover: "bg-red-500", }, } @@ -117,12 +118,17 @@ export const IndexingStatusDot: React.FC = ({ className "hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", "active:bg-[rgba(255,255,255,0.1)]", + "cursor-pointer", className, )} aria-label={getTooltipText()}> + {/* File search icon */} + + + {/* Status dot badge */} diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index 86e200f0a3..75324c97f4 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -38,8 +38,8 @@ vi.mock("@src/context/ExtensionStateContext") const getEnhancePromptButton = () => { return screen.getByRole("button", { name: (_, element) => { - // Find the button with the sparkle icon - return element.querySelector(".codicon-sparkle") !== null + // Find the button with the wand sparkles icon (Lucide React) + return element.querySelector(".lucide-wand-sparkles") !== null }, }) } @@ -154,8 +154,9 @@ describe("ChatTextArea", () => { const enhanceButton = getEnhancePromptButton() fireEvent.click(enhanceButton) - const loadingSpinner = screen.getByText("", { selector: ".codicon-loading" }) - expect(loadingSpinner).toBeInTheDocument() + // Check if the WandSparkles icon has the animate-spin class + const animatingIcon = enhanceButton.querySelector(".animate-spin") + expect(animatingIcon).toBeInTheDocument() }) }) diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index 8a44082f58..37eb291530 100644 --- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" import { vscode } from "@src/utils/vscode" -import { IndexingStatusDot } from "../IndexingStatusBadge" +import { IndexingStatusBadge } from "../IndexingStatusBadge" vi.mock("@/i18n/setup", () => ({ __esModule: true, @@ -104,9 +104,9 @@ vi.mock("@/i18n/TranslationContext", () => ({ }), })) -describe("IndexingStatusDot", () => { +describe("IndexingStatusBadge", () => { const renderComponent = (props = {}) => { - return render() + return render() } beforeEach(() => { diff --git a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx new file mode 100644 index 0000000000..4ef949f9e0 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx @@ -0,0 +1,61 @@ +// Tests for VERTEX_REGIONS "global" region handling + +import { describe, it, expect } from "vitest" +import { VERTEX_REGIONS } from "../../../../../../packages/types/src/providers/vertex" + +describe("VERTEX_REGIONS", () => { + it('should include the "global" region as the first entry', () => { + expect(VERTEX_REGIONS[0]).toEqual({ value: "global", label: "global" }) + }) + + it('should contain "global" region exactly once', () => { + const globalRegions = VERTEX_REGIONS.filter((r: { value: string; label: string }) => r.value === "global") + expect(globalRegions).toHaveLength(1) + }) + + it('should contain all expected regions including "global"', () => { + // The expected list is the imported VERTEX_REGIONS itself + expect(VERTEX_REGIONS).toEqual([ + { value: "global", label: "global" }, + { value: "us-central1", label: "us-central1" }, + { value: "us-east1", label: "us-east1" }, + { value: "us-east4", label: "us-east4" }, + { value: "us-east5", label: "us-east5" }, + { value: "us-west1", label: "us-west1" }, + { value: "us-west2", label: "us-west2" }, + { value: "us-west3", label: "us-west3" }, + { value: "us-west4", label: "us-west4" }, + { value: "northamerica-northeast1", label: "northamerica-northeast1" }, + { value: "northamerica-northeast2", label: "northamerica-northeast2" }, + { value: "southamerica-east1", label: "southamerica-east1" }, + { value: "europe-west1", label: "europe-west1" }, + { value: "europe-west2", label: "europe-west2" }, + { value: "europe-west3", label: "europe-west3" }, + { value: "europe-west4", label: "europe-west4" }, + { value: "europe-west6", label: "europe-west6" }, + { value: "europe-central2", label: "europe-central2" }, + { value: "asia-east1", label: "asia-east1" }, + { value: "asia-east2", label: "asia-east2" }, + { value: "asia-northeast1", label: "asia-northeast1" }, + { value: "asia-northeast2", label: "asia-northeast2" }, + { value: "asia-northeast3", label: "asia-northeast3" }, + { value: "asia-south1", label: "asia-south1" }, + { value: "asia-south2", label: "asia-south2" }, + { value: "asia-southeast1", label: "asia-southeast1" }, + { value: "asia-southeast2", label: "asia-southeast2" }, + { value: "australia-southeast1", label: "australia-southeast1" }, + { value: "australia-southeast2", label: "australia-southeast2" }, + { value: "me-west1", label: "me-west1" }, + { value: "me-central1", label: "me-central1" }, + { value: "africa-south1", label: "africa-south1" }, + ]) + }) + + it('should contain "asia-east1" region exactly once', () => { + const asiaEast1Regions = VERTEX_REGIONS.filter( + (r: { value: string; label: string }) => r.value === "asia-east1" && r.label === "asia-east1", + ) + expect(asiaEast1Regions).toHaveLength(1) + expect(asiaEast1Regions[0]).toEqual({ value: "asia-east1", label: "asia-east1" }) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 848306a1cf..6accf68fcb 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Millora la sol·licitud amb context addicional", "addImages": "Afegeix imatges al missatge", "sendMessage": "Envia el missatge", + "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", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "ADVERTÈNCIA: S'ha activat una substitució personalitzada d'instruccions del sistema. Això pot trencar greument la funcionalitat i causar un comportament impredictible.", - "profileViolationWarning": "El perfil actual infringeix la configuració de la teva organització", + "profileViolationWarning": "El perfil actual no és compatible amb la configuració de la teva organització", "shellIntegration": { "title": "Advertència d'execució d'ordres", "description": "La teva ordre s'està executant sense la integració de shell del terminal VSCode. Per suprimir aquest advertiment, pots desactivar la integració de shell a la secció Terminal de la configuració de Roo Code o solucionar problemes d'integració del terminal VSCode utilitzant l'enllaç a continuació.", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index f28448ce54..fcbbdb2b68 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Prompt mit zusätzlichem Kontext verbessern", "addImages": "Bilder zur Nachricht hinzufügen", "sendMessage": "Nachricht senden", + "stopTts": "Text-in-Sprache beenden", "typeMessage": "Nachricht eingeben...", "typeTask": "Gib deine Aufgabe hier ein...", "addContext": "@ für Kontext, / zum Moduswechsel", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "WARNUNG: Benutzerdefinierte Systemaufforderung aktiv. Dies kann die Funktionalität erheblich beeinträchtigen und zu unvorhersehbarem Verhalten führen.", - "profileViolationWarning": "Das aktuelle Profil verstößt gegen die Einstellungen deiner Organisation", + "profileViolationWarning": "Das aktuelle Profil ist nicht kompatibel mit den Einstellungen deiner Organisation", "shellIntegration": { "title": "Befehlsausführungswarnung", "description": "Dein Befehl wird ohne VSCode Terminal-Shell-Integration ausgeführt. Um diese Warnung zu unterdrücken, kannst du die Shell-Integration im Abschnitt Terminal der Roo Code Einstellungen deaktivieren oder die VSCode Terminal-Integration mit dem Link unten beheben.", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 0c49fbaac7..326f52c74d 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -120,6 +120,7 @@ "enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", "addImages": "Add images to message", "sendMessage": "Send message", + "stopTts": "Stop text-to-speech", "typeMessage": "Type a message...", "typeTask": "Type your task here...", "addContext": "@ to add context, / to switch modes", @@ -293,7 +294,7 @@ } }, "systemPromptWarning": "WARNING: Custom system prompt override active. This can severely break functionality and cause unpredictable behavior.", - "profileViolationWarning": "The current profile violates your organization's settings", + "profileViolationWarning": "The current profile isn't compatible with your organization's settings", "shellIntegration": { "title": "Command Execution Warning", "description": "Your command is being executed without VSCode terminal shell integration. To suppress this warning you can disable shell integration in the Terminal section of the Roo Code settings or troubleshoot VSCode terminal integration using the link below.", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 71a26f6c10..9bea6bd2c4 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Mejorar el mensaje con contexto adicional", "addImages": "Agregar imágenes al mensaje", "sendMessage": "Enviar mensaje", + "stopTts": "Detener texto a voz", "typeMessage": "Escribe un mensaje...", "typeTask": "Escribe tu tarea aquí...", "addContext": "@ para agregar contexto, / para cambiar modos", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "ADVERTENCIA: Anulación de instrucciones del sistema personalizada activa. Esto puede romper gravemente la funcionalidad y causar un comportamiento impredecible.", - "profileViolationWarning": "El perfil actual infringe la configuración de tu organización", + "profileViolationWarning": "El perfil actual no es compatible con la configuración de tu organización", "shellIntegration": { "title": "Advertencia de ejecución de comandos", "description": "Tu comando se está ejecutando sin la integración de shell de terminal de VSCode. Para suprimir esta advertencia, puedes desactivar la integración de shell en la sección Terminal de la configuración de Roo Code o solucionar problemas de integración de terminal de VSCode usando el enlace de abajo.", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 19531f2554..d7748fb15c 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Améliorer la requête avec un contexte supplémentaire", "addImages": "Ajouter des images au message", "sendMessage": "Envoyer le message", + "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", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "AVERTISSEMENT : Remplacement d'instructions système personnalisées actif. Cela peut gravement perturber la fonctionnalité et provoquer un comportement imprévisible.", - "profileViolationWarning": "Le profil actuel enfreint les paramètres de votre organisation", + "profileViolationWarning": "Le profil actuel n'est pas compatible avec les paramètres de votre organisation", "shellIntegration": { "title": "Avertissement d'exécution de commande", "description": "Votre commande est exécutée sans l'intégration shell du terminal VSCode. Pour supprimer cet avertissement, vous pouvez désactiver l'intégration shell dans la section Terminal des paramètres de Roo Code ou résoudre les problèmes d'intégration du terminal VSCode en utilisant le lien ci-dessous.", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index a45317d2da..7979f624e0 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ", "addImages": "संदेश में चित्र जोड़ें", "sendMessage": "संदेश भेजें", + "stopTts": "टेक्स्ट-टू-स्पीच बंद करें", "typeMessage": "एक संदेश लिखें...", "typeTask": "अपना कार्य यहां लिखें...", "addContext": "संदर्भ जोड़ने के लिए @, मोड बदलने के लिए /", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "चेतावनी: कस्टम सिस्टम प्रॉम्प्ट ओवरराइड सक्रिय है। यह कार्यक्षमता को गंभीर रूप से बाधित कर सकता है और अनियमित व्यवहार का कारण बन सकता है.", - "profileViolationWarning": "वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स का उल्लंघन करती है", + "profileViolationWarning": "वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है", "shellIntegration": { "title": "कमांड निष्पादन चेतावनी", "description": "आपका कमांड VSCode टर्मिनल शेल इंटीग्रेशन के बिना निष्पादित हो रहा है। इस चेतावनी को दबाने के लिए आप Roo Code सेटिंग्स के Terminal अनुभाग में शेल इंटीग्रेशन को अक्षम कर सकते हैं या नीचे दिए गए लिंक का उपयोग करके VSCode टर्मिनल इंटीग्रेशन की समस्या का समाधान कर सकते हैं।", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 7df325b7f9..2134b48bfc 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -126,6 +126,7 @@ }, "addImages": "Tambahkan gambar ke pesan", "sendMessage": "Kirim pesan", + "stopTts": "Hentikan text-to-speech", "typeMessage": "Ketik pesan...", "typeTask": "Bangun, cari, tanya sesuatu", "addContext": "@ untuk menambah konteks, / untuk ganti mode", @@ -300,7 +301,7 @@ } }, "systemPromptWarning": "PERINGATAN: Override system prompt kustom aktif. Ini dapat merusak fungsionalitas secara serius dan menyebabkan perilaku yang tidak terduga.", - "profileViolationWarning": "Profil saat ini melanggar pengaturan organisasi kamu", + "profileViolationWarning": "Profil saat ini tidak kompatibel dengan pengaturan organisasi kamu", "shellIntegration": { "title": "Peringatan Eksekusi Perintah", "description": "Perintah kamu dijalankan tanpa integrasi shell terminal VSCode. Untuk menekan peringatan ini kamu bisa menonaktifkan integrasi shell di bagian Terminal dari pengaturan Roo Code atau troubleshoot integrasi terminal VSCode menggunakan link di bawah.", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 4b368f214f..ed886e6ef9 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Migliora prompt con contesto aggiuntivo", "addImages": "Aggiungi immagini al messaggio", "sendMessage": "Invia messaggio", + "stopTts": "Interrompi sintesi vocale", "typeMessage": "Scrivi un messaggio...", "typeTask": "Scrivi la tua attività qui...", "addContext": "@ per aggiungere contesto, / per cambiare modalità", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "ATTENZIONE: Sovrascrittura personalizzata delle istruzioni di sistema attiva. Questo può compromettere gravemente le funzionalità e causare comportamenti imprevedibili.", - "profileViolationWarning": "Il profilo corrente viola le impostazioni della tua organizzazione", + "profileViolationWarning": "Il profilo corrente non è compatibile con le impostazioni della tua organizzazione", "shellIntegration": { "title": "Avviso di esecuzione comando", "description": "Il tuo comando viene eseguito senza l'integrazione shell del terminale VSCode. Per sopprimere questo avviso puoi disattivare l'integrazione shell nella sezione Terminal delle impostazioni di Roo Code o risolvere i problemi di integrazione del terminale VSCode utilizzando il link qui sotto.", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 5a19b64945..61c2820299 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "追加コンテキストでプロンプトを強化", "addImages": "メッセージに画像を追加", "sendMessage": "メッセージを送信", + "stopTts": "テキスト読み上げを停止", "typeMessage": "メッセージを入力...", "typeTask": "ここにタスクを入力...", "addContext": "コンテキスト追加は@、モード切替は/", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "警告:カスタムシステムプロンプトの上書きが有効です。これにより機能が深刻に損なわれ、予測不可能な動作が発生する可能性があります。", - "profileViolationWarning": "現在のプロファイルは組織の設定に違反しています", + "profileViolationWarning": "現在のプロファイルは組織の設定と互換性がありません", "shellIntegration": { "title": "コマンド実行警告", "description": "コマンドはVSCodeターミナルシェル統合なしで実行されています。この警告を非表示にするには、Roo Code設定Terminalセクションでシェル統合を無効にするか、以下のリンクを使用してVSCodeターミナル統合のトラブルシューティングを行ってください。", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index f3ed7699ca..9ac8e90644 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "추가 컨텍스트로 프롬프트 향상", "addImages": "메시지에 이미지 추가", "sendMessage": "메시지 보내기", + "stopTts": "텍스트 음성 변환 중지", "typeMessage": "메시지 입력...", "typeTask": "여기에 작업 입력...", "addContext": "컨텍스트 추가는 @, 모드 전환은 /", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "경고: 사용자 정의 시스템 프롬프트 재정의가 활성화되었습니다. 이로 인해 기능이 심각하게 손상되고 예측할 수 없는 동작이 발생할 수 있습니다.", - "profileViolationWarning": "현재 프로필이 조직 설정을 위반합니다", + "profileViolationWarning": "현재 프로필이 조직 설정과 호환되지 않습니다", "shellIntegration": { "title": "명령 실행 경고", "description": "명령이 VSCode 터미널 쉘 통합 없이 실행되고 있습니다. 이 경고를 숨기려면 Roo Code 설정Terminal 섹션에서 쉘 통합을 비활성화하거나 아래 링크를 사용하여 VSCode 터미널 통합 문제를 해결하세요.", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 874fb74bc7..5b2da04160 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -112,6 +112,7 @@ }, "addImages": "Afbeeldingen toevoegen aan bericht", "sendMessage": "Bericht verzenden", + "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", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "WAARSCHUWING: Aangepaste systeemprompt actief. Dit kan de functionaliteit ernstig verstoren en onvoorspelbaar gedrag veroorzaken.", - "profileViolationWarning": "Het huidige profiel schendt de instellingen van uw organisatie", + "profileViolationWarning": "Het huidige profiel is niet compatibel met de instellingen van uw organisatie", "shellIntegration": { "title": "Waarschuwing commando-uitvoering", "description": "Je commando wordt uitgevoerd zonder VSCode-terminal shell-integratie. Om deze waarschuwing te onderdrukken kun je shell-integratie uitschakelen in het gedeelte Terminal van de Roo Code-instellingen of de VSCode-terminalintegratie oplossen via de onderstaande link.", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 254f18c9ba..5226ef51ea 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Ulepsz podpowiedź dodatkowym kontekstem", "addImages": "Dodaj obrazy do wiadomości", "sendMessage": "Wyślij wiadomość", + "stopTts": "Zatrzymaj syntezę mowy", "typeMessage": "Wpisz wiadomość...", "typeTask": "Wpisz swoje zadanie tutaj...", "addContext": "@ aby dodać kontekst, / aby zmienić tryb", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "OSTRZEŻENIE: Aktywne niestandardowe zastąpienie instrukcji systemowych. Może to poważnie zakłócić funkcjonalność i powodować nieprzewidywalne zachowanie.", - "profileViolationWarning": "Bieżący profil narusza ustawienia Twojej organizacji", + "profileViolationWarning": "Bieżący profil nie jest kompatybilny z ustawieniami Twojej organizacji", "shellIntegration": { "title": "Ostrzeżenie wykonania polecenia", "description": "Twoje polecenie jest wykonywane bez integracji powłoki terminala VSCode. Aby ukryć to ostrzeżenie, możesz wyłączyć integrację powłoki w sekcji Terminal w ustawieniach Roo Code lub rozwiązać problemy z integracją terminala VSCode korzystając z poniższego linku.", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index bebfe0c722..eca50fbd33 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Aprimorar prompt com contexto adicional", "addImages": "Adicionar imagens à mensagem", "sendMessage": "Enviar mensagem", + "stopTts": "Parar conversão de texto em fala", "typeMessage": "Digite uma mensagem...", "typeTask": "Digite sua tarefa aqui...", "addContext": "@ para adicionar contexto, / para alternar modos", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "AVISO: Substituição personalizada de instrução do sistema ativa. Isso pode comprometer gravemente a funcionalidade e causar comportamento imprevisível.", - "profileViolationWarning": "O perfil atual viola as configurações da sua organização", + "profileViolationWarning": "O perfil atual não é compatível com as configurações da sua organização", "shellIntegration": { "title": "Aviso de execução de comando", "description": "Seu comando está sendo executado sem a integração de shell do terminal VSCode. Para suprimir este aviso, você pode desativar a integração de shell na seção Terminal das configurações do Roo Code ou solucionar problemas de integração do terminal VSCode usando o link abaixo.", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 4ac8f4854a..72ae883703 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -112,6 +112,7 @@ }, "addImages": "Добавить изображения к сообщению", "sendMessage": "Отправить сообщение", + "stopTts": "Остановить синтез речи", "typeMessage": "Введите сообщение...", "typeTask": "Введите вашу задачу здесь...", "addContext": "@ для добавления контекста, / для смены режима", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "ПРЕДУПРЕЖДЕНИЕ: Активна пользовательская системная подсказка. Это может серьезно нарушить работу и вызвать непредсказуемое поведение.", - "profileViolationWarning": "Текущий профиль нарушает настройки вашей организации", + "profileViolationWarning": "Текущий профиль несовместим с настройками вашей организации", "shellIntegration": { "title": "Предупреждение о выполнении команды", "description": "Ваша команда выполняется без интеграции оболочки терминала VSCode. Чтобы скрыть это предупреждение, вы можете отключить интеграцию оболочки в разделе Terminal в настройках Roo Code или устранить проблемы с интеграцией терминала VSCode, используя ссылку ниже.", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 56a9f54cb7..4f108d7ac3 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Ek bağlamla istemi geliştir", "addImages": "Mesaja resim ekle", "sendMessage": "Mesaj gönder", + "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 /", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "UYARI: Özel sistem komut geçersiz kılma aktif. Bu işlevselliği ciddi şekilde bozabilir ve öngörülemeyen davranışlara neden olabilir.", - "profileViolationWarning": "Geçerli profil kuruluşunuzun ayarlarını ihlal ediyor", + "profileViolationWarning": "Geçerli profil kuruluşunuzun ayarlarıyla uyumlu değil", "shellIntegration": { "title": "Komut Çalıştırma Uyarısı", "description": "Komutunuz VSCode terminal kabuk entegrasyonu olmadan çalıştırılıyor. Bu uyarıyı gizlemek için Roo Code ayarları'nın Terminal bölümünden kabuk entegrasyonunu devre dışı bırakabilir veya aşağıdaki bağlantıyı kullanarak VSCode terminal entegrasyonu sorunlarını giderebilirsiniz.", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 574df08da1..0f0ea71ec3 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "Nâng cao yêu cầu với ngữ cảnh bổ sung", "addImages": "Thêm hình ảnh vào tin nhắn", "sendMessage": "Gửi tin nhắn", + "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ế độ", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "CẢNH BÁO: Đã kích hoạt ghi đè lệnh nhắc hệ thống tùy chỉnh. Điều này có thể phá vỡ nghiêm trọng chức năng và gây ra hành vi không thể dự đoán.", - "profileViolationWarning": "Hồ sơ hiện tại vi phạm cài đặt của tổ chức của bạn", + "profileViolationWarning": "Hồ sơ hiện tại không tương thích với cài đặt của tổ chức của bạn", "shellIntegration": { "title": "Cảnh báo thực thi lệnh", "description": "Lệnh của bạn đang được thực thi mà không có tích hợp shell terminal VSCode. Để ẩn cảnh báo này, bạn có thể vô hiệu hóa tích hợp shell trong phần Terminal của cài đặt Roo Code hoặc khắc phục sự cố tích hợp terminal VSCode bằng liên kết bên dưới.", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index f6e51cfa49..6e883b751a 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "增强提示词", "addImages": "添加图片到消息", "sendMessage": "发送消息", + "stopTts": "停止文本转语音", "typeMessage": "输入消息...", "typeTask": "在此处输入您的任务...", "addContext": "@添加上下文,/切换模式", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "警告:自定义系统提示词覆盖已激活。这可能严重破坏功能并导致不可预测的行为。", - "profileViolationWarning": "当前配置文件违反了您的组织设置", + "profileViolationWarning": "当前配置文件与您的组织设置不兼容", "shellIntegration": { "title": "命令执行警告", "description": "您的命令正在没有 VSCode 终端 shell 集成的情况下执行。要隐藏此警告,您可以在 Roo Code 设置Terminal 部分禁用 shell 集成,或使用下方链接排查 VSCode 终端集成问题。", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 54aeacf15c..75d214db03 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -105,6 +105,7 @@ "enhancePrompt": "使用額外內容增強提示", "addImages": "新增圖片到訊息中", "sendMessage": "傳送訊息", + "stopTts": "停止文字轉語音", "typeMessage": "輸入訊息...", "typeTask": "在此處輸入您的工作...", "addContext": "輸入 @ 新增內容,輸入 / 切換模式", @@ -280,7 +281,7 @@ } }, "systemPromptWarning": "警告:自訂系統提示詞覆蓋已啟用。這可能嚴重破壞功能並導致不可預測的行為。", - "profileViolationWarning": "目前設定檔違反了您的組織設定", + "profileViolationWarning": "目前設定檔與您的組織設定不相容", "shellIntegration": { "title": "命令執行警告", "description": "您的命令正在沒有 VSCode 終端機 shell 整合的情況下執行。要隱藏此警告,您可以在 Roo Code 設定Terminal 部分停用 shell 整合,或使用下方連結排查 VSCode 終端機整合問題。",