Merge branch 'RooCodeInc:main' into main

This commit is contained in:
Murilo Pires 2025-07-10 12:24:08 -03:00 committed by GitHub
commit 05ccf57dc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1174 additions and 224 deletions

View file

@ -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.

View file

@ -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

View file

@ -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" },
]

View file

@ -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<string, ModelInfo>

View file

@ -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<Registe
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const contextProxy = await ContextProxy.getInstance(context)
const codeIndexManager = CodeIndexManager.getInstance(context)
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager)
// Get the existing MDM service instance to ensure consistent policy enforcement
let mdmService: MdmService | undefined
try {
mdmService = MdmService.getInstance()
} catch (error) {
// MDM service not initialized, which is fine - extension can work without it
mdmService = undefined
}
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager, mdmService)
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
// Check if there are any visible text editors, otherwise open a new group

View file

@ -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")
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है",

View file

@ -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",

View file

@ -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",

View file

@ -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": "コンテキストは最近圧縮されました;この試行をスキップします",

View file

@ -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": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Контекст был недавно сжат; пропускаем эту попытку",

View file

@ -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",

View file

@ -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",

View file

@ -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": "上下文最近已压缩;跳过此次尝试",

View file

@ -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": "上下文最近已壓縮;跳過此次嘗試",

View file

@ -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()
})
})
})

View file

@ -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[] = []

View file

@ -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"
}
]

View file

@ -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

View file

@ -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") + "/")
})
})

View file

@ -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") + "/")
})
})

View file

@ -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<string[]> {
if (!recursive) {
return [] // Only needed for recursive mode
async function createIgnoreInstance(dirPath: string): Promise<ReturnType<typeof ignore>> {
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<string[]> {
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<typeof ignore>,
): Promise<string[]> {
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<typeof ignore>,
): 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

View file

@ -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

View file

@ -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")
})
})
})

View file

@ -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<HTMLTextAreaElement, ChatTextAreaProps>(
/>
{isTtsPlaying && (
<Button
variant="ghost"
size="icon"
className="absolute top-0 right-0 opacity-25 hover:opacity-100 z-10"
onClick={() => vscode.postMessage({ type: "stopTts" })}>
<VolumeX className="size-4" />
</Button>
<StandardTooltip content={t("chat:stopTts")}>
<Button
variant="ghost"
size="icon"
className="absolute top-0 right-0 opacity-25 hover:opacity-100 z-10"
onClick={() => vscode.postMessage({ type: "stopTts" })}>
<VolumeX className="size-4" />
</Button>
</StandardTooltip>
)}
<div className="absolute top-1 right-1 z-30">
<IconButton
iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
title={t("chat:enhancePrompt")}
disabled={sendingDisabled}
isLoading={isEnhancingPrompt}
onClick={handleEnhancePrompt}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
<StandardTooltip content={t("chat:enhancePrompt")}>
<button
aria-label={t("chat:enhancePrompt")}
disabled={sendingDisabled}
onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-150",
"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)]",
!sendingDisabled && "cursor-pointer",
sendingDisabled &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
)}>
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
</button>
</StandardTooltip>
</div>
<div className="absolute bottom-1 right-1 z-30">
<IconButton
iconClass="codicon-send"
title={t("chat:sendMessage")}
disabled={sendingDisabled}
onClick={onSend}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
<StandardTooltip content={t("chat:sendMessage")}>
<button
aria-label={t("chat:sendMessage")}
disabled={sendingDisabled}
onClick={!sendingDisabled ? onSend : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-150",
"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)]",
!sendingDisabled && "cursor-pointer",
sendingDisabled &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
)}>
<SendHorizontal className="w-4 h-4" />
</button>
</StandardTooltip>
</div>
{!inputValue && (
@ -1145,14 +1173,29 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</div>
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
<IndexingStatusDot />
<IconButton
iconClass="codicon-device-camera"
title={t("chat:addImages")}
disabled={shouldDisableImages}
onClick={onSelectImages}
className="mr-1"
/>
<IndexingStatusBadge />
<StandardTooltip content={t("chat:addImages")}>
<button
aria-label={t("chat:addImages")}
disabled={shouldDisableImages}
onClick={!shouldDisableImages ? onSelectImages : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-foreground opacity-85",
"transition-all duration-150",
"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)]",
!shouldDisableImages && "cursor-pointer",
shouldDisableImages &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
"mr-1",
)}>
<Image className="w-4 h-4" />
</button>
</StandardTooltip>
</div>
</div>
</div>

View file

@ -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<IndexingStatusDotProps> = ({ className }) => {
export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ 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<IndexingStatusDotProps> = ({ 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<IndexingStatusDotProps> = ({ 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 */}
<Database className="w-4 h-4 text-vscode-foreground" />
{/* Status dot badge */}
<span
className={cn(
"inline-block w-2 h-2 rounded-full relative z-10 transition-colors duration-200",
"absolute top-1 right-1 w-1.5 h-1.5 rounded-full transition-colors duration-200",
getStatusColorClass(),
)}
/>

View file

@ -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()
})
})

View file

@ -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(<IndexingStatusDot {...props} />)
return render(<IndexingStatusBadge {...props} />)
}
beforeEach(() => {

View file

@ -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" })
})
})

View file

@ -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ó <strong>Terminal</strong> de la <settingsLink>configuració de Roo Code</settingsLink> o solucionar problemes d'integració del terminal VSCode utilitzant l'enllaç a continuació.",

View file

@ -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 <strong>Terminal</strong> der <settingsLink>Roo Code Einstellungen</settingsLink> deaktivieren oder die VSCode Terminal-Integration mit dem Link unten beheben.",

View file

@ -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 <strong>Terminal</strong> section of the <settingsLink>Roo Code settings</settingsLink> or troubleshoot VSCode terminal integration using the link below.",

View file

@ -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 <strong>Terminal</strong> de la <settingsLink>configuración de Roo Code</settingsLink> o solucionar problemas de integración de terminal de VSCode usando el enlace de abajo.",

View file

@ -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 <strong>Terminal</strong> des <settingsLink>paramètres de Roo Code</settingsLink> ou résoudre les problèmes d'intégration du terminal VSCode en utilisant le lien ci-dessous.",

View file

@ -105,6 +105,7 @@
"enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ",
"addImages": "संदेश में चित्र जोड़ें",
"sendMessage": "संदेश भेजें",
"stopTts": "टेक्स्ट-टू-स्पीच बंद करें",
"typeMessage": "एक संदेश लिखें...",
"typeTask": "अपना कार्य यहां लिखें...",
"addContext": "संदर्भ जोड़ने के लिए @, मोड बदलने के लिए /",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "चेतावनी: कस्टम सिस्टम प्रॉम्प्ट ओवरराइड सक्रिय है। यह कार्यक्षमता को गंभीर रूप से बाधित कर सकता है और अनियमित व्यवहार का कारण बन सकता है.",
"profileViolationWarning": "वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स का उल्लंघन करती है",
"profileViolationWarning": "वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है",
"shellIntegration": {
"title": "कमांड निष्पादन चेतावनी",
"description": "आपका कमांड VSCode टर्मिनल शेल इंटीग्रेशन के बिना निष्पादित हो रहा है। इस चेतावनी को दबाने के लिए आप <settingsLink>Roo Code सेटिंग्स</settingsLink> के <strong>Terminal</strong> अनुभाग में शेल इंटीग्रेशन को अक्षम कर सकते हैं या नीचे दिए गए लिंक का उपयोग करके VSCode टर्मिनल इंटीग्रेशन की समस्या का समाधान कर सकते हैं।",

View file

@ -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 <strong>Terminal</strong> dari <settingsLink>pengaturan Roo Code</settingsLink> atau troubleshoot integrasi terminal VSCode menggunakan link di bawah.",

View file

@ -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 <strong>Terminal</strong> delle <settingsLink>impostazioni di Roo Code</settingsLink> o risolvere i problemi di integrazione del terminale VSCode utilizzando il link qui sotto.",

View file

@ -105,6 +105,7 @@
"enhancePrompt": "追加コンテキストでプロンプトを強化",
"addImages": "メッセージに画像を追加",
"sendMessage": "メッセージを送信",
"stopTts": "テキスト読み上げを停止",
"typeMessage": "メッセージを入力...",
"typeTask": "ここにタスクを入力...",
"addContext": "コンテキスト追加は@、モード切替は/",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "警告:カスタムシステムプロンプトの上書きが有効です。これにより機能が深刻に損なわれ、予測不可能な動作が発生する可能性があります。",
"profileViolationWarning": "現在のプロファイルは組織の設定に違反しています",
"profileViolationWarning": "現在のプロファイルは組織の設定と互換性がありません",
"shellIntegration": {
"title": "コマンド実行警告",
"description": "コマンドはVSCodeターミナルシェル統合なしで実行されています。この警告を非表示にするには、<settingsLink>Roo Code設定</settingsLink>の<strong>Terminal</strong>セクションでシェル統合を無効にするか、以下のリンクを使用してVSCodeターミナル統合のトラブルシューティングを行ってください。",

View file

@ -105,6 +105,7 @@
"enhancePrompt": "추가 컨텍스트로 프롬프트 향상",
"addImages": "메시지에 이미지 추가",
"sendMessage": "메시지 보내기",
"stopTts": "텍스트 음성 변환 중지",
"typeMessage": "메시지 입력...",
"typeTask": "여기에 작업 입력...",
"addContext": "컨텍스트 추가는 @, 모드 전환은 /",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "경고: 사용자 정의 시스템 프롬프트 재정의가 활성화되었습니다. 이로 인해 기능이 심각하게 손상되고 예측할 수 없는 동작이 발생할 수 있습니다.",
"profileViolationWarning": "현재 프로필이 조직 설정을 위반합니다",
"profileViolationWarning": "현재 프로필이 조직 설정과 호환되지 않습니다",
"shellIntegration": {
"title": "명령 실행 경고",
"description": "명령이 VSCode 터미널 쉘 통합 없이 실행되고 있습니다. 이 경고를 숨기려면 <settingsLink>Roo Code 설정</settingsLink>의 <strong>Terminal</strong> 섹션에서 쉘 통합을 비활성화하거나 아래 링크를 사용하여 VSCode 터미널 통합 문제를 해결하세요.",

View file

@ -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 <strong>Terminal</strong> van de <settingsLink>Roo Code-instellingen</settingsLink> of de VSCode-terminalintegratie oplossen via de onderstaande link.",

View file

@ -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 <strong>Terminal</strong> w <settingsLink>ustawieniach Roo Code</settingsLink> lub rozwiązać problemy z integracją terminala VSCode korzystając z poniższego linku.",

View file

@ -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 <strong>Terminal</strong> das <settingsLink>configurações do Roo Code</settingsLink> ou solucionar problemas de integração do terminal VSCode usando o link abaixo.",

View file

@ -112,6 +112,7 @@
},
"addImages": "Добавить изображения к сообщению",
"sendMessage": "Отправить сообщение",
"stopTts": "Остановить синтез речи",
"typeMessage": "Введите сообщение...",
"typeTask": "Введите вашу задачу здесь...",
"addContext": "@ для добавления контекста, / для смены режима",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "ПРЕДУПРЕЖДЕНИЕ: Активна пользовательская системная подсказка. Это может серьезно нарушить работу и вызвать непредсказуемое поведение.",
"profileViolationWarning": "Текущий профиль нарушает настройки вашей организации",
"profileViolationWarning": "Текущий профиль несовместим с настройками вашей организации",
"shellIntegration": {
"title": "Предупреждение о выполнении команды",
"description": "Ваша команда выполняется без интеграции оболочки терминала VSCode. Чтобы скрыть это предупреждение, вы можете отключить интеграцию оболочки в разделе <strong>Terminal</strong> в <settingsLink>настройках Roo Code</settingsLink> или устранить проблемы с интеграцией терминала VSCode, используя ссылку ниже.",

View file

@ -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 <settingsLink>Roo Code ayarları</settingsLink>'nın <strong>Terminal</strong> bölümünden kabuk entegrasyonunu devre dışı bırakabilir veya aşağıdaki bağlantıyı kullanarak VSCode terminal entegrasyonu sorunlarını giderebilirsiniz.",

View file

@ -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 <strong>Terminal</strong> của <settingsLink>cài đặt Roo Code</settingsLink> hoặc khắc phục sự cố tích hợp terminal VSCode bằng liên kết bên dưới.",

View file

@ -105,6 +105,7 @@
"enhancePrompt": "增强提示词",
"addImages": "添加图片到消息",
"sendMessage": "发送消息",
"stopTts": "停止文本转语音",
"typeMessage": "输入消息...",
"typeTask": "在此处输入您的任务...",
"addContext": "@添加上下文,/切换模式",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "警告:自定义系统提示词覆盖已激活。这可能严重破坏功能并导致不可预测的行为。",
"profileViolationWarning": "当前配置文件违反了您的组织设置",
"profileViolationWarning": "当前配置文件与您的组织设置不兼容",
"shellIntegration": {
"title": "命令执行警告",
"description": "您的命令正在没有 VSCode 终端 shell 集成的情况下执行。要隐藏此警告,您可以在 <settingsLink>Roo Code 设置</settingsLink>的 <strong>Terminal</strong> 部分禁用 shell 集成,或使用下方链接排查 VSCode 终端集成问题。",

View file

@ -105,6 +105,7 @@
"enhancePrompt": "使用額外內容增強提示",
"addImages": "新增圖片到訊息中",
"sendMessage": "傳送訊息",
"stopTts": "停止文字轉語音",
"typeMessage": "輸入訊息...",
"typeTask": "在此處輸入您的工作...",
"addContext": "輸入 @ 新增內容,輸入 / 切換模式",
@ -280,7 +281,7 @@
}
},
"systemPromptWarning": "警告:自訂系統提示詞覆蓋已啟用。這可能嚴重破壞功能並導致不可預測的行為。",
"profileViolationWarning": "目前設定檔違反了您的組織設定",
"profileViolationWarning": "目前設定檔與您的組織設定不相容",
"shellIntegration": {
"title": "命令執行警告",
"description": "您的命令正在沒有 VSCode 終端機 shell 整合的情況下執行。要隱藏此警告,您可以在 <settingsLink>Roo Code 設定</settingsLink>的 <strong>Terminal</strong> 部分停用 shell 整合,或使用下方連結排查 VSCode 終端機整合問題。",