Merge branch 'main' into will/edit-delete-overhaul

This commit is contained in:
Will Li 2025-07-10 20:46:31 -07:00
commit 69c3996739
95 changed files with 1809 additions and 437 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
@ -365,7 +383,7 @@
- Fix vscode-material-icons in the filer picker
- Fix global settings export
- Respect user-configured terminal integration timeout (thanks @KJ7LNW)
- Contex condensing enhancements (thanks @SannidhyaSah)
- Context condensing enhancements (thanks @SannidhyaSah)
## [3.18.1] - 2025-05-22
@ -877,7 +895,7 @@
## [3.10.1] - 2025-03-20
- Make the suggested responses optional to not break overriden system prompts
- Make the suggested responses optional to not break overridden system prompts
## [3.10.0] - 2025-03-20

View file

@ -224,7 +224,7 @@ function validateInput(input) {
},
text: `Use apply_diff on the file ${testFile.name} to change "Hello World" to "Hello Universe". The file already exists with this content:
${testFile.content}\nAssume the file exists and you can modify it directly.`,
}) //Temporary meassure since list_files ignores all the files inside a tmp workspace
}) //Temporary measure since list_files ignores all the files inside a tmp workspace
console.log("Task ID:", taskId)
console.log("Test filename:", testFile.name)

View file

@ -203,7 +203,7 @@ export function Evals({
</ScatterChart>
</ChartContainer>
<div className="py-4 text-xs opacity-50">
(Note: Very expensive models are exluded from the scatter plot.)
(Note: Very expensive models are excluded from the scatter plot.)
</div>
</TableCaption>
</Table>

View file

@ -29,7 +29,7 @@ Start the evals service:
docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0
```
The initial build process can take a minute or two. Upon success you should see ouput indicating that a web service is running on [localhost:3000](http://localhost:3000/):
The initial build process can take a minute or two. Upon success you should see output indicating that a web service is running on [localhost:3000](http://localhost:3000/):
<img width="1182" alt="Screenshot 2025-06-05 at 12 05 38PM" src="https://github.com/user-attachments/assets/34f25a59-1362-458c-aafa-25e13cdb2a7a" />
Additionally, you'll find in Docker Desktop that database and redis services are running:

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

@ -36,7 +36,7 @@ export function getReadFileToolDescription(blockName: string, blockParams: any):
}
} catch (error) {
console.error("Failed to parse read_file args XML for description:", error)
return `[${blockName} with unparseable args]`
return `[${blockName} with unparsable args]`
}
} else if (blockParams.path) {
// Fallback for legacy single-path usage

View file

@ -53,7 +53,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "No s'ha pogut processar el lot després de {{maxRetries}} intents: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "No s'ha pogut connectar a la base de dades vectorial Qdrant. Assegura't que Qdrant estigui funcionant i sigui accessible a {{qdrantUrl}}. Error: {{errorMessage}}"
"qdrantConnectionFailed": "No s'ha pogut connectar a la base de dades vectorial Qdrant. Assegura't que Qdrant estigui funcionant i sigui accessible a {{qdrantUrl}}. Error: {{errorMessage}}",
"vectorDimensionMismatch": "No s'ha pogut actualitzar l'índex de vectors per al nou model. Prova d'esborrar l'índex i tornar a començar. Detalls: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Ha fallat l'autenticació. Comproveu la vostra clau d'API a la configuració.",
@ -36,7 +37,9 @@
"invalidApiKey": "Clau d'API no vàlida. Comproveu la vostra configuració de clau d'API.",
"invalidBaseUrl": "URL base no vàlida. Comproveu la vostra configuració d'URL.",
"invalidModel": "Model no vàlid. Comproveu la vostra configuració de model.",
"invalidResponse": "Resposta no vàlida del servei d'incrustació. Comproveu la vostra configuració."
"invalidResponse": "Resposta no vàlida del servei d'incrustació. Comproveu la vostra configuració.",
"apiKeyRequired": "Es requereix una clau d'API per a aquest incrustador",
"baseUrlRequired": "Es requereix una URL base per a aquest incrustador"
},
"serviceFactory": {
"openAiConfigMissing": "Falta la configuració d'OpenAI per crear l'embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Verarbeitung des Batches nach {{maxRetries}} Versuchen fehlgeschlagen: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Verbindung zur Qdrant-Vektordatenbank fehlgeschlagen. Stelle sicher, dass Qdrant läuft und unter {{qdrantUrl}} erreichbar ist. Fehler: {{errorMessage}}"
"qdrantConnectionFailed": "Verbindung zur Qdrant-Vektordatenbank fehlgeschlagen. Stelle sicher, dass Qdrant läuft und unter {{qdrantUrl}} erreichbar ist. Fehler: {{errorMessage}}",
"vectorDimensionMismatch": "Aktualisierung des Vektorindex für neues Modell fehlgeschlagen. Bitte versuche, den Index zu löschen und von vorne zu beginnen. Details: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfe deinen API-Schlüssel in den Einstellungen.",
@ -36,7 +37,9 @@
"invalidApiKey": "Ungültiger API-Schlüssel. Bitte überprüfe deine API-Schlüssel-Konfiguration.",
"invalidBaseUrl": "Ungültige Basis-URL. Bitte überprüfe deine URL-Konfiguration.",
"invalidModel": "Ungültiges Modell. Bitte überprüfe deine Modellkonfiguration.",
"invalidResponse": "Ungültige Antwort vom Embedder-Dienst. Bitte überprüfe deine Konfiguration."
"invalidResponse": "Ungültige Antwort vom Embedder-Dienst. Bitte überprüfe deine Konfiguration.",
"apiKeyRequired": "API-Schlüssel ist für diesen Embedder erforderlich",
"baseUrlRequired": "Basis-URL ist für diesen Embedder erforderlich"
},
"serviceFactory": {
"openAiConfigMissing": "OpenAI-Konfiguration fehlt für die Erstellung des Embedders",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Failed to process batch after {{maxRetries}} attempts: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}"
"qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}",
"vectorDimensionMismatch": "Failed to update vector index for new model. Please try clearing the index and starting again. Details: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authentication failed. Please check your API key in the settings.",
@ -36,7 +37,9 @@
"invalidApiKey": "Invalid API key. Please check your API key configuration.",
"invalidBaseUrl": "Invalid base URL. Please check your URL configuration.",
"invalidModel": "Invalid model. Please check your model configuration.",
"invalidResponse": "Invalid response from embedder service. Please check your configuration."
"invalidResponse": "Invalid response from embedder service. Please check your configuration.",
"apiKeyRequired": "API key is required for this embedder",
"baseUrlRequired": "Base URL is required for this embedder"
},
"serviceFactory": {
"openAiConfigMissing": "OpenAI configuration missing for embedder creation",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Error al procesar lote después de {{maxRetries}} intentos: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Error al conectar con la base de datos vectorial Qdrant. Asegúrate de que Qdrant esté funcionando y sea accesible en {{qdrantUrl}}. Error: {{errorMessage}}"
"qdrantConnectionFailed": "Error al conectar con la base de datos vectorial Qdrant. Asegúrate de que Qdrant esté funcionando y sea accesible en {{qdrantUrl}}. Error: {{errorMessage}}",
"vectorDimensionMismatch": "No se pudo actualizar el índice de vectores para el nuevo modelo. Intenta borrar el índice y empezar de nuevo. Detalles: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Error de autenticación. Comprueba tu clave de API en los ajustes.",
@ -36,7 +37,9 @@
"invalidApiKey": "Clave de API no válida. Comprueba la configuración de tu clave de API.",
"invalidBaseUrl": "URL base no válida. Comprueba la configuración de tu URL.",
"invalidModel": "Modelo no válido. Comprueba la configuración de tu modelo.",
"invalidResponse": "Respuesta no válida del servicio de embedder. Comprueba tu configuración."
"invalidResponse": "Respuesta no válida del servicio de embedder. Comprueba tu configuración.",
"apiKeyRequired": "Se requiere una clave de API para este embedder",
"baseUrlRequired": "Se requiere una URL base para este embedder"
},
"serviceFactory": {
"openAiConfigMissing": "Falta la configuración de OpenAI para crear el incrustador",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Échec du traitement du lot après {{maxRetries}} tentatives : {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Échec de la connexion à la base de données vectorielle Qdrant. Veuillez vous assurer que Qdrant fonctionne et est accessible à {{qdrantUrl}}. Erreur : {{errorMessage}}"
"qdrantConnectionFailed": "Échec de la connexion à la base de données vectorielle Qdrant. Veuillez vous assurer que Qdrant fonctionne et est accessible à {{qdrantUrl}}. Erreur : {{errorMessage}}",
"vectorDimensionMismatch": "Échec de la mise à jour de l'index vectoriel pour le nouveau modèle. Veuillez essayer de vider l'index et de recommencer. Détails : {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Échec de l'authentification. Veuillez vérifier votre clé API dans les paramètres.",
@ -36,7 +37,9 @@
"invalidApiKey": "Clé API invalide. Veuillez vérifier votre configuration de clé API.",
"invalidBaseUrl": "URL de base invalide. Veuillez vérifier votre configuration d'URL.",
"invalidModel": "Modèle invalide. Veuillez vérifier votre configuration de modèle.",
"invalidResponse": "Réponse invalide du service d'embedder. Veuillez vérifier votre configuration."
"invalidResponse": "Réponse invalide du service d'embedder. Veuillez vérifier votre configuration.",
"apiKeyRequired": "Une clé API est requise pour cet embedder.",
"baseUrlRequired": "Une URL de base est requise pour cet embedder"
},
"serviceFactory": {
"openAiConfigMissing": "Configuration OpenAI manquante pour la création de l'embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "{{maxRetries}} प्रयासों के बाद बैच प्रसंस्करण विफल: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant वेक्टर डेटाबेस से कनेक्ट करने में विफल। कृपया सुनिश्चित करें कि Qdrant चल रहा है और {{qdrantUrl}} पर पहुंच योग्य है। त्रुटि: {{errorMessage}}"
"qdrantConnectionFailed": "Qdrant वेक्टर डेटाबेस से कनेक्ट करने में विफल। कृपया सुनिश्चित करें कि Qdrant चल रहा है और {{qdrantUrl}} पर पहुंच योग्य है। त्रुटि: {{errorMessage}}",
"vectorDimensionMismatch": "नए मॉडल के लिए वेक्टर इंडेक्स को अपडेट करने में विफल। कृपया इंडेक्स को साफ़ करने और फिर से शुरू करने का प्रयास करें। विवरण: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "प्रमाणीकरण विफल। कृपया सेटिंग्स में अपनी एपीआई कुंजी जांचें।",
@ -36,7 +37,9 @@
"invalidApiKey": "अमान्य एपीआई कुंजी। कृपया अपनी एपीआई कुंजी कॉन्फ़िगरेशन जांचें।",
"invalidBaseUrl": "अमान्य बेस यूआरएल। कृपया अपनी यूआरएल कॉन्फ़िगरेशन जांचें।",
"invalidModel": "अमान्य मॉडल। कृपया अपनी मॉडल कॉन्फ़िगरेशन जांचें।",
"invalidResponse": "एम्बेडर सेवा से अमान्य प्रतिक्रिया। कृपया अपनी कॉन्फ़िगरेशन जांचें।"
"invalidResponse": "एम्बेडर सेवा से अमान्य प्रतिक्रिया। कृपया अपनी कॉन्फ़िगरेशन जांचें।",
"apiKeyRequired": "इस एम्बेडर के लिए API कुंजी आवश्यक है।",
"baseUrlRequired": "इस एम्बेडर के लिए बेस यूआरएल आवश्यक है"
},
"serviceFactory": {
"openAiConfigMissing": "एम्बेडर बनाने के लिए OpenAI कॉन्फ़िगरेशन गायब है",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Gagal memproses batch setelah {{maxRetries}} percobaan: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Gagal terhubung ke database vektor Qdrant. Pastikan Qdrant berjalan dan dapat diakses di {{qdrantUrl}}. Error: {{errorMessage}}"
"qdrantConnectionFailed": "Gagal terhubung ke database vektor Qdrant. Pastikan Qdrant berjalan dan dapat diakses di {{qdrantUrl}}. Error: {{errorMessage}}",
"vectorDimensionMismatch": "Gagal memperbarui indeks vektor untuk model baru. Silakan coba bersihkan indeks dan mulai lagi. Detail: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Autentikasi gagal. Silakan periksa kunci API Anda di pengaturan.",
@ -36,7 +37,9 @@
"invalidApiKey": "Kunci API tidak valid. Silakan periksa konfigurasi kunci API Anda.",
"invalidBaseUrl": "URL dasar tidak valid. Silakan periksa konfigurasi URL Anda.",
"invalidModel": "Model tidak valid. Silakan periksa konfigurasi model Anda.",
"invalidResponse": "Respons tidak valid dari layanan embedder. Silakan periksa konfigurasi Anda."
"invalidResponse": "Respons tidak valid dari layanan embedder. Silakan periksa konfigurasi Anda.",
"apiKeyRequired": "Kunci API diperlukan untuk embedder ini",
"baseUrlRequired": "URL dasar diperlukan untuk embedder ini"
},
"serviceFactory": {
"openAiConfigMissing": "Konfigurasi OpenAI tidak ada untuk membuat embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Elaborazione del batch fallita dopo {{maxRetries}} tentativi: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Impossibile connettersi al database vettoriale Qdrant. Assicurati che Qdrant sia in esecuzione e accessibile su {{qdrantUrl}}. Errore: {{errorMessage}}"
"qdrantConnectionFailed": "Impossibile connettersi al database vettoriale Qdrant. Assicurati che Qdrant sia in esecuzione e accessibile su {{qdrantUrl}}. Errore: {{errorMessage}}",
"vectorDimensionMismatch": "Impossibile aggiornare l'indice vettoriale per il nuovo modello. Prova a cancellare l'indice e a ricominciare. Dettagli: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Autenticazione fallita. Controlla la tua chiave API nelle impostazioni.",
@ -36,7 +37,9 @@
"invalidApiKey": "Chiave API non valida. Controlla la configurazione della tua chiave API.",
"invalidBaseUrl": "URL di base non valido. Controlla la configurazione del tuo URL.",
"invalidModel": "Modello non valido. Controlla la configurazione del tuo modello.",
"invalidResponse": "Risposta non valida dal servizio embedder. Controlla la tua configurazione."
"invalidResponse": "Risposta non valida dal servizio embedder. Controlla la tua configurazione.",
"apiKeyRequired": "È richiesta una chiave API per questo embedder",
"baseUrlRequired": "È richiesto un URL di base per questo embedder"
},
"serviceFactory": {
"openAiConfigMissing": "Configurazione OpenAI mancante per la creazione dell'embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "{{maxRetries}}回の試行後、バッチ処理に失敗しました:{{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrantベクターデータベースへの接続に失敗しました。Qdrantが実行中で{{qdrantUrl}}でアクセス可能であることを確認してください。エラー:{{errorMessage}}"
"qdrantConnectionFailed": "Qdrantベクターデータベースへの接続に失敗しました。Qdrantが実行中で{{qdrantUrl}}でアクセス可能であることを確認してください。エラー:{{errorMessage}}",
"vectorDimensionMismatch": "新しいモデルのベクトルインデックスの更新に失敗しました。インデックスをクリアして再試行してください。詳細:{{errorMessage}}"
},
"validation": {
"authenticationFailed": "認証に失敗しました。設定でAPIキーを確認してください。",
@ -36,7 +37,9 @@
"invalidApiKey": "無効なAPIキーです。APIキー構成を確認してください。",
"invalidBaseUrl": "無効なベースURLです。URL構成を確認してください。",
"invalidModel": "無効なモデルです。モデル構成を確認してください。",
"invalidResponse": "エンベッダーサービスからの無効な応答です。設定を確認してください。"
"invalidResponse": "エンベッダーサービスからの無効な応答です。設定を確認してください。",
"apiKeyRequired": "このエンベッダーにはAPIキーが必要です。",
"baseUrlRequired": "このエンベッダーにはベースURLが必要です"
},
"serviceFactory": {
"openAiConfigMissing": "エンベッダー作成のためのOpenAI設定がありません",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "{{maxRetries}}번 시도 후 배치 처리 실패: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant 벡터 데이터베이스에 연결하지 못했습니다. Qdrant가 실행 중이고 {{qdrantUrl}}에서 접근 가능한지 확인하세요. 오류: {{errorMessage}}"
"qdrantConnectionFailed": "Qdrant 벡터 데이터베이스에 연결하지 못했습니다. Qdrant가 실행 중이고 {{qdrantUrl}}에서 접근 가능한지 확인하세요. 오류: {{errorMessage}}",
"vectorDimensionMismatch": "새 모델의 벡터 인덱스를 업데이트하지 못했습니다. 인덱스를 지우고 다시 시작해 보세요. 세부 정보: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "인증에 실패했습니다. 설정에서 API 키를 확인하세요.",
@ -36,7 +37,9 @@
"invalidApiKey": "잘못된 API 키입니다. API 키 구성을 확인하세요.",
"invalidBaseUrl": "잘못된 기본 URL입니다. URL 구성을 확인하세요.",
"invalidModel": "잘못된 모델입니다. 모델 구성을 확인하세요.",
"invalidResponse": "임베더 서비스에서 잘못된 응답이 왔습니다. 구성을 확인하세요."
"invalidResponse": "임베더 서비스에서 잘못된 응답이 왔습니다. 구성을 확인하세요.",
"apiKeyRequired": "이 임베더에는 API 키가 필요합니다",
"baseUrlRequired": "이 임베더에는 기본 URL이 필요합니다"
},
"serviceFactory": {
"openAiConfigMissing": "임베더 생성을 위한 OpenAI 구성이 누락되었습니다",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Verwerken van batch mislukt na {{maxRetries}} pogingen: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Kan geen verbinding maken met Qdrant vectordatabase. Zorg ervoor dat Qdrant draait en toegankelijk is op {{qdrantUrl}}. Fout: {{errorMessage}}"
"qdrantConnectionFailed": "Kan geen verbinding maken met Qdrant vectordatabase. Zorg ervoor dat Qdrant draait en toegankelijk is op {{qdrantUrl}}. Fout: {{errorMessage}}",
"vectorDimensionMismatch": "Kan de vectorindex voor het nieuwe model niet bijwerken. Probeer de index te wissen en opnieuw te beginnen. Details: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authenticatie mislukt. Controleer je API-sleutel in de instellingen.",
@ -36,7 +37,9 @@
"invalidApiKey": "Ongeldige API-sleutel. Controleer je API-sleutelconfiguratie.",
"invalidBaseUrl": "Ongeldige basis-URL. Controleer je URL-configuratie.",
"invalidModel": "Ongeldig model. Controleer je modelconfiguratie.",
"invalidResponse": "Ongeldige reactie van embedder-service. Controleer je configuratie."
"invalidResponse": "Ongeldige reactie van embedder-service. Controleer je configuratie.",
"apiKeyRequired": "API-sleutel is vereist voor deze embedder",
"baseUrlRequired": "Basis-URL is vereist voor deze embedder"
},
"serviceFactory": {
"openAiConfigMissing": "OpenAI-configuratie ontbreekt voor het maken van embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Nie udało się przetworzyć partii po {{maxRetries}} próbach: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Nie udało się połączyć z bazą danych wektorowych Qdrant. Upewnij się, że Qdrant jest uruchomiony i dostępny pod adresem {{qdrantUrl}}. Błąd: {{errorMessage}}"
"qdrantConnectionFailed": "Nie udało się połączyć z bazą danych wektorowych Qdrant. Upewnij się, że Qdrant jest uruchomiony i dostępny pod adresem {{qdrantUrl}}. Błąd: {{errorMessage}}",
"vectorDimensionMismatch": "Nie udało się zaktualizować indeksu wektorowego dla nowego modelu. Spróbuj wyczyścić indeks i zacząć od nowa. Szczegóły: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Uwierzytelnianie nie powiodło się. Sprawdź swój klucz API w ustawieniach.",
@ -36,7 +37,9 @@
"invalidApiKey": "Nieprawidłowy klucz API. Sprawdź konfigurację klucza API.",
"invalidBaseUrl": "Nieprawidłowy podstawowy adres URL. Sprawdź konfigurację adresu URL.",
"invalidModel": "Nieprawidłowy model. Sprawdź konfigurację modelu.",
"invalidResponse": "Nieprawidłowa odpowiedź z usługi embedder. Sprawdź swoją konfigurację."
"invalidResponse": "Nieprawidłowa odpowiedź z usługi embedder. Sprawdź swoją konfigurację.",
"apiKeyRequired": "Klucz API jest wymagany dla tego embeddera",
"baseUrlRequired": "Podstawowy adres URL jest wymagany dla tego embeddera"
},
"serviceFactory": {
"openAiConfigMissing": "Brak konfiguracji OpenAI do utworzenia embeddera",

View file

@ -53,7 +53,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Falha ao processar lote após {{maxRetries}} tentativas: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Falha ao conectar com o banco de dados vetorial Qdrant. Certifique-se de que o Qdrant esteja rodando e acessível em {{qdrantUrl}}. Erro: {{errorMessage}}"
"qdrantConnectionFailed": "Falha ao conectar com o banco de dados vetorial Qdrant. Certifique-se de que o Qdrant esteja rodando e acessível em {{qdrantUrl}}. Erro: {{errorMessage}}",
"vectorDimensionMismatch": "Falha ao atualizar o índice de vetores para o novo modelo. Tente limpar o índice e começar novamente. Detalhes: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Falha na autenticação. Verifique sua chave de API nas configurações.",
@ -36,7 +37,9 @@
"invalidApiKey": "Chave de API inválida. Verifique sua configuração de chave de API.",
"invalidBaseUrl": "URL base inválida. Verifique sua configuração de URL.",
"invalidModel": "Modelo inválido. Verifique a configuração do seu modelo.",
"invalidResponse": "Resposta inválida do serviço de embedder. Verifique sua configuração."
"invalidResponse": "Resposta inválida do serviço de embedder. Verifique sua configuração.",
"apiKeyRequired": "A chave de API é necessária para este embedder",
"baseUrlRequired": "A URL base é necessária para este embedder"
},
"serviceFactory": {
"openAiConfigMissing": "Configuração do OpenAI ausente para criação do embedder",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Не удалось обработать пакет после {{maxRetries}} попыток: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Не удалось подключиться к векторной базе данных Qdrant. Убедитесь, что Qdrant запущен и доступен по адресу {{qdrantUrl}}. Ошибка: {{errorMessage}}"
"qdrantConnectionFailed": "Не удалось подключиться к векторной базе данных Qdrant. Убедитесь, что Qdrant запущен и доступен по адресу {{qdrantUrl}}. Ошибка: {{errorMessage}}",
"vectorDimensionMismatch": "Не удалось обновить векторный индекс для новой модели. Попробуйте очистить индекс и начать сначала. Подробности: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Ошибка аутентификации. Проверьте свой ключ API в настройках.",
@ -36,7 +37,9 @@
"invalidApiKey": "Неверный ключ API. Проверьте конфигурацию ключа API.",
"invalidBaseUrl": "Неверный базовый URL. Проверьте конфигурацию URL.",
"invalidModel": "Неверная модель. Проверьте конфигурацию модели.",
"invalidResponse": "Неверный ответ от службы embedder. Проверьте вашу конфигурацию."
"invalidResponse": "Неверный ответ от службы embedder. Проверьте вашу конфигурацию.",
"apiKeyRequired": "Для этого встраивателя требуется ключ API",
"baseUrlRequired": "Для этого встраивателя требуется базовый URL"
},
"serviceFactory": {
"openAiConfigMissing": "Отсутствует конфигурация OpenAI для создания эмбеддера",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "{{maxRetries}} denemeden sonra toplu işlem başarısız oldu: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant vektör veritabanına bağlanılamadı. Qdrant'ın çalıştığından ve {{qdrantUrl}} adresinde erişilebilir olduğundan emin olun. Hata: {{errorMessage}}"
"qdrantConnectionFailed": "Qdrant vektör veritabanına bağlanılamadı. Qdrant'ın çalıştığından ve {{qdrantUrl}} adresinde erişilebilir olduğundan emin olun. Hata: {{errorMessage}}",
"vectorDimensionMismatch": "Yeni model için vektör dizini güncellenemedi. Lütfen dizini temizleyip yeniden başlatmayı deneyin. Detaylar: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Kimlik doğrulama başarısız oldu. Lütfen ayarlardan API anahtarınızı kontrol edin.",
@ -36,7 +37,9 @@
"invalidApiKey": "Geçersiz API anahtarı. Lütfen API anahtarı yapılandırmanızı kontrol edin.",
"invalidBaseUrl": "Geçersiz temel URL. Lütfen URL yapılandırmanızı kontrol edin.",
"invalidModel": "Geçersiz model. Lütfen model yapılandırmanızı kontrol edin.",
"invalidResponse": "Embedder hizmetinden geçersiz yanıt. Lütfen yapılandırmanızı kontrol edin."
"invalidResponse": "Embedder hizmetinden geçersiz yanıt. Lütfen yapılandırmanızı kontrol edin.",
"apiKeyRequired": "Bu gömücü için API anahtarı gereklidir",
"baseUrlRequired": "Bu gömücü için temel URL gereklidir"
},
"serviceFactory": {
"openAiConfigMissing": "Gömücü oluşturmak için OpenAI yapılandırması eksik",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "Không thể xử lý lô sau {{maxRetries}} lần thử: {{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "Không thể kết nối với cơ sở dữ liệu vector Qdrant. Vui lòng đảm bảo Qdrant đang chạy và có thể truy cập tại {{qdrantUrl}}. Lỗi: {{errorMessage}}"
"qdrantConnectionFailed": "Không thể kết nối với cơ sở dữ liệu vector Qdrant. Vui lòng đảm bảo Qdrant đang chạy và có thể truy cập tại {{qdrantUrl}}. Lỗi: {{errorMessage}}",
"vectorDimensionMismatch": "Không thể cập nhật chỉ mục vector cho mô hình mới. Vui lòng thử xóa chỉ mục và bắt đầu lại. Chi tiết: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Xác thực không thành công. Vui lòng kiểm tra khóa API của bạn trong cài đặt.",
@ -36,7 +37,9 @@
"invalidApiKey": "Khóa API không hợp lệ. Vui lòng kiểm tra cấu hình khóa API của bạn.",
"invalidBaseUrl": "URL cơ sở không hợp lệ. Vui lòng kiểm tra cấu hình URL của bạn.",
"invalidModel": "Mô hình không hợp lệ. Vui lòng kiểm tra cấu hình mô hình của bạn.",
"invalidResponse": "Phản hồi không hợp lệ từ dịch vụ embedder. Vui lòng kiểm tra cấu hình của bạn."
"invalidResponse": "Phản hồi không hợp lệ từ dịch vụ embedder. Vui lòng kiểm tra cấu hình của bạn.",
"apiKeyRequired": "Cần có khóa API cho trình nhúng này",
"baseUrlRequired": "Cần có URL cơ sở cho trình nhúng này"
},
"serviceFactory": {
"openAiConfigMissing": "Thiếu cấu hình OpenAI để tạo embedder",

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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "尝试 {{maxRetries}} 次后批次处理失败:{{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "连接 Qdrant 向量数据库失败。请确保 Qdrant 正在运行并可在 {{qdrantUrl}} 访问。错误:{{errorMessage}}"
"qdrantConnectionFailed": "连接 Qdrant 向量数据库失败。请确保 Qdrant 正在运行并可在 {{qdrantUrl}} 访问。错误:{{errorMessage}}",
"vectorDimensionMismatch": "无法更新新模型的向量索引。请尝试清除索引并重新开始。详细信息:{{errorMessage}}"
},
"validation": {
"authenticationFailed": "身份验证失败。请在设置中检查您的 API 密钥。",
@ -36,7 +37,9 @@
"invalidApiKey": "API 密钥无效。请检查您的 API 密钥配置。",
"invalidBaseUrl": "基础 URL 无效。请检查您的 URL 配置。",
"invalidModel": "模型无效。请检查您的模型配置。",
"invalidResponse": "嵌入服务响应无效。请检查您的配置。"
"invalidResponse": "嵌入服务响应无效。请检查您的配置。",
"apiKeyRequired": "此嵌入器需要 API 密钥",
"baseUrlRequired": "此嵌入器需要基础 URL"
},
"serviceFactory": {
"openAiConfigMissing": "创建嵌入器缺少 OpenAI 配置",

View file

@ -49,7 +49,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

@ -23,7 +23,8 @@
"failedToProcessBatchWithError": "嘗試 {{maxRetries}} 次後批次處理失敗:{{errorMessage}}"
},
"vectorStore": {
"qdrantConnectionFailed": "連接 Qdrant 向量資料庫失敗。請確保 Qdrant 正在執行並可在 {{qdrantUrl}} 存取。錯誤:{{errorMessage}}"
"qdrantConnectionFailed": "連接 Qdrant 向量資料庫失敗。請確保 Qdrant 正在執行並可在 {{qdrantUrl}} 存取。錯誤:{{errorMessage}}",
"vectorDimensionMismatch": "無法更新新模型的向量索引。請嘗試清除索引並重新開始。詳細資訊: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "驗證失敗。請在設定中檢查您的 API 金鑰。",
@ -36,7 +37,9 @@
"invalidApiKey": "無效的 API 金鑰。請檢查您的 API 金鑰組態。",
"invalidBaseUrl": "無效的基礎 URL。請檢查您的 URL 組態。",
"invalidModel": "無效的模型。請檢查您的模型組態。",
"invalidResponse": "內嵌服務回應無效。請檢查您的組態。"
"invalidResponse": "內嵌服務回應無效。請檢查您的組態。",
"apiKeyRequired": "此嵌入器需要 API 金鑰",
"baseUrlRequired": "此嵌入器需要基礎 URL"
},
"serviceFactory": {
"openAiConfigMissing": "建立嵌入器缺少 OpenAI 設定",

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

@ -34,9 +34,9 @@ describe("GeminiEmbedder", () => {
it("should throw error when API key is not provided", () => {
// Act & Assert
expect(() => new GeminiEmbedder("")).toThrow("API key is required for Gemini embedder")
expect(() => new GeminiEmbedder(null as any)).toThrow("API key is required for Gemini embedder")
expect(() => new GeminiEmbedder(undefined as any)).toThrow("API key is required for Gemini embedder")
expect(() => new GeminiEmbedder("")).toThrow("validation.apiKeyRequired")
expect(() => new GeminiEmbedder(null as any)).toThrow("validation.apiKeyRequired")
expect(() => new GeminiEmbedder(undefined as any)).toThrow("validation.apiKeyRequired")
})
})

View file

@ -80,19 +80,19 @@ describe("OpenAICompatibleEmbedder", () => {
it("should throw error when baseUrl is missing", () => {
expect(() => new OpenAICompatibleEmbedder("", testApiKey, testModelId)).toThrow(
"Base URL is required for OpenAI Compatible embedder",
"embeddings:validation.baseUrlRequired",
)
})
it("should throw error when apiKey is missing", () => {
expect(() => new OpenAICompatibleEmbedder(testBaseUrl, "", testModelId)).toThrow(
"API key is required for OpenAI Compatible embedder",
"embeddings:validation.apiKeyRequired",
)
})
it("should throw error when both baseUrl and apiKey are missing", () => {
expect(() => new OpenAICompatibleEmbedder("", "", testModelId)).toThrow(
"Base URL is required for OpenAI Compatible embedder",
"embeddings:validation.baseUrlRequired",
)
})
})

View file

@ -1,6 +1,7 @@
import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { GEMINI_MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
/**
* Gemini embedder implementation that wraps the OpenAI Compatible embedder
@ -23,7 +24,7 @@ export class GeminiEmbedder implements IEmbedder {
*/
constructor(apiKey: string) {
if (!apiKey) {
throw new Error("API key is required for Gemini embedder")
throw new Error(t("embeddings:validation.apiKeyRequired"))
}
// Create an OpenAI Compatible embedder with Gemini's fixed configuration

View file

@ -45,10 +45,10 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
*/
constructor(baseUrl: string, apiKey: string, modelId?: string, maxItemTokens?: number) {
if (!baseUrl) {
throw new Error("Base URL is required for OpenAI Compatible embedder")
throw new Error(t("embeddings:validation.baseUrlRequired"))
}
if (!apiKey) {
throw new Error("API key is required for OpenAI Compatible embedder")
throw new Error(t("embeddings:validation.apiKeyRequired"))
}
this.baseUrl = baseUrl

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,9 @@ 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"
this._stateManager.setSystemState("Error", errorMessage)
throw new Error(errorMessage)
}
// (Re)Initialize orchestrator

View file

@ -28,16 +28,16 @@ export function getErrorMessageForStatus(status: number | undefined, embedderTyp
switch (status) {
case 401:
case 403:
return "embeddings:validation.authenticationFailed"
return t("embeddings:validation.authenticationFailed")
case 404:
return embedderType === "openai"
? "embeddings:validation.modelNotAvailable"
: "embeddings:validation.invalidEndpoint"
? t("embeddings:validation.modelNotAvailable")
: t("embeddings:validation.invalidEndpoint")
case 429:
return "embeddings:validation.serviceUnavailable"
return t("embeddings:validation.serviceUnavailable")
default:
if (status && status >= 400 && status < 600) {
return "embeddings:validation.configurationError"
return t("embeddings:validation.configurationError")
}
return undefined
}
@ -138,11 +138,11 @@ export function handleValidationError(
errorMessage.includes("HTTP 0:") ||
errorMessage === "No response"
) {
return { valid: false, error: "embeddings:validation.connectionFailed" }
return { valid: false, error: t("embeddings:validation.connectionFailed") }
}
if (errorMessage.includes("Failed to parse response JSON")) {
return { valid: false, error: "embeddings:validation.invalidResponse" }
return { valid: false, error: t("embeddings:validation.invalidResponse") }
}
}
@ -152,7 +152,7 @@ export function handleValidationError(
}
// Fallback to generic error
return { valid: false, error: "embeddings:validation.configurationError" }
return { valid: false, error: t("embeddings:validation.configurationError") }
}
/**

View file

@ -9,6 +9,9 @@ import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../../cons
vitest.mock("@qdrant/js-client-rest")
vitest.mock("crypto")
vitest.mock("../../../../utils/path")
vitest.mock("../../../../i18n", () => ({
t: (key: string) => key, // Just return the key for testing
}))
vitest.mock("path", () => ({
...vitest.importActual("path"),
sep: "/",
@ -674,7 +677,7 @@ describe("QdrantVectorStore", () => {
;(console.warn as any).mockRestore()
})
it("should re-throw error from deleteCollection when recreating collection with mismatched vectorSize", async () => {
it("should throw vectorDimensionMismatch error when deleteCollection fails during recreation", async () => {
const differentVectorSize = 768
mockQdrantClientInstance.getCollection.mockResolvedValue({
config: {
@ -691,15 +694,67 @@ describe("QdrantVectorStore", () => {
vitest.spyOn(console, "error").mockImplementation(() => {})
vitest.spyOn(console, "warn").mockImplementation(() => {})
// The actual error message includes the URL and error details
await expect(vectorStore.initialize()).rejects.toThrow(
/Failed to connect to Qdrant vector database|vectorStore\.qdrantConnectionFailed/,
)
// The error should have a cause property set to the original error
let caughtError: any
try {
await vectorStore.initialize()
} catch (error: any) {
caughtError = error
}
expect(caughtError).toBeDefined()
expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
expect(caughtError.cause).toBe(deleteError)
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
// Should log both the warning and the critical error
expect(console.warn).toHaveBeenCalledTimes(1)
expect(console.error).toHaveBeenCalledTimes(2) // One for the critical error, one for the outer catch
;(console.error as any).mockRestore()
;(console.warn as any).mockRestore()
})
it("should throw vectorDimensionMismatch error when createCollection fails during recreation", async () => {
const differentVectorSize = 768
mockQdrantClientInstance.getCollection.mockResolvedValue({
config: {
params: {
vectors: {
size: differentVectorSize,
},
},
},
} as any)
// Delete succeeds but create fails
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
const createError = new Error("Create Collection Failed")
mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
vitest.spyOn(console, "error").mockImplementation(() => {})
vitest.spyOn(console, "warn").mockImplementation(() => {})
// Should throw an error with cause property set to the original error
let caughtError: any
try {
await vectorStore.initialize()
} catch (error: any) {
caughtError = error
}
expect(caughtError).toBeDefined()
expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
expect(caughtError.cause).toBe(createError)
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
// Should log warning, critical error, and outer error
expect(console.warn).toHaveBeenCalledTimes(1)
expect(console.error).toHaveBeenCalledTimes(2)
;(console.error as any).mockRestore()
;(console.warn as any).mockRestore()
})

View file

@ -165,17 +165,33 @@ export class QdrantVectorStore implements IVectorStore {
created = false // Exists and correct
} else {
// Exists but wrong vector size, recreate
console.warn(
`[QdrantVectorStore] Collection ${this.collectionName} exists with vector size ${existingVectorSize}, but expected ${this.vectorSize}. Recreating collection.`,
)
await this.client.deleteCollection(this.collectionName) // Known to exist
await this.client.createCollection(this.collectionName, {
vectors: {
size: this.vectorSize,
distance: this.DISTANCE_METRIC,
},
})
created = true
try {
console.warn(
`[QdrantVectorStore] Collection ${this.collectionName} exists with vector size ${existingVectorSize}, but expected ${this.vectorSize}. Recreating collection.`,
)
await this.client.deleteCollection(this.collectionName)
await this.client.createCollection(this.collectionName, {
vectors: {
size: this.vectorSize,
distance: this.DISTANCE_METRIC,
},
})
created = true
} catch (recreationError) {
const errorMessage =
recreationError instanceof Error ? recreationError.message : String(recreationError)
console.error(
`[QdrantVectorStore] CRITICAL: Failed to recreate collection ${this.collectionName} for new vector size. Error: ${errorMessage}`,
)
const dimensionMismatchError = new Error(
t("embeddings:vectorStore.vectorDimensionMismatch", {
errorMessage,
}),
)
// Use error.cause to preserve the original error context
dimensionMismatchError.cause = recreationError
throw dimensionMismatchError
}
}
}
@ -204,7 +220,12 @@ export class QdrantVectorStore implements IVectorStore {
errorMessage,
)
// Provide a more user-friendly error message that includes the original error
// If this is already a vector dimension mismatch error (identified by cause), re-throw it as-is
if (error instanceof Error && error.cause !== undefined) {
throw error
}
// Otherwise, provide a more user-friendly error message that includes the original error
throw new Error(
t("embeddings:vectorStore.qdrantConnectionFailed", { qdrantUrl: this.qdrantUrl, errorMessage }),
)

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,38 +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

@ -1198,9 +1198,9 @@ export const ChatRowContent = ({
return <div>Error displaying search results.</div>
}
const { query = "", results = [] } = parsed?.content || {}
const { results = [] } = parsed?.content || {}
return <CodebaseSearchResultsDisplay query={query} results={results} />
return <CodebaseSearchResultsDisplay results={results} />
case "user_edit_todos":
return <UpdateTodoListToolBlock userEdited onChange={() => {}} />
default:

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"
@ -832,20 +831,48 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
className="text-xs bg-vscode-toolbar-hoverBackground hover:bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground">
Cancel
</Button>
<IconButton
iconClass="codicon-device-camera"
title={t("chat:addImages")}
disabled={shouldDisableImages}
onClick={onSelectImages}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
<IconButton
iconClass="codicon-edit"
title={t("chat:save.tooltip")}
disabled={sendingDisabled}
onClick={onSend}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
<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]",
"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)]",
!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",
)}>
<Image className="w-4 h-4" />
</button>
</StandardTooltip>
<StandardTooltip content={t("chat:save.tooltip")}>
<button
aria-label={t("chat:save.tooltip")}
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>
</div>
)
@ -989,14 +1016,49 @@ 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"
/>
{isTtsPlaying && (
<StandardTooltip content={t("chat:stopTts")}>
<button
aria-label={t("chat:stopTts")}
onClick={() => vscode.postMessage({ type: "stopTts" })}
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)]",
"cursor-pointer",
)}>
<VolumeX className="w-4 h-4" />
</button>
</StandardTooltip>
)}
<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>
)
@ -1100,36 +1162,53 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onScroll={() => updateHighlights()}
/>
{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>
)}
<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>
{!isEditMode && (
<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>
)}

View file

@ -30,15 +30,15 @@ const CodebaseSearchResult: React.FC<CodebaseSearchResultProps> = ({ filePath, s
<StandardTooltip content={t("codebaseSearch.resultTooltip", { score: score.toFixed(3) })}>
<div
onClick={handleClick}
className="mb-1 p-2 border border-primary rounded cursor-pointer hover:bg-secondary hover:text-white">
className="p-2 border border-[var(--vscode-editorGroup-border)] cursor-pointer hover:bg-secondary hover:text-white">
<div className="flex gap-2 items-center overflow-hidden">
<span className="text-primary-300 whitespace-nowrap flex-shrink-0">
{filePath.split("/").at(-1)}:{startLine}-{endLine}
{filePath.split("/").at(-1)}:{startLine === endLine ? startLine : `${startLine}-${endLine}`}
</span>
<span className="text-gray-500 truncate min-w-0 flex-1">
{filePath.split("/").slice(0, -1).join("/")}
</span>
<span className="text-xs text-vscode-descriptionForeground bg-vscode-badge-background px-2 py-1 rounded whitespace-nowrap ml-auto">
<span className="text-xs text-vscode-descriptionForeground whitespace-nowrap ml-auto opacity-60">
{score.toFixed(3)}
</span>
</div>

View file

@ -3,7 +3,6 @@ import CodebaseSearchResult from "./CodebaseSearchResult"
import { Trans } from "react-i18next"
interface CodebaseSearchResultsDisplayProps {
query: string
results: Array<{
filePath: string
score: number
@ -13,26 +12,26 @@ interface CodebaseSearchResultsDisplayProps {
}>
}
const CodebaseSearchResultsDisplay: React.FC<CodebaseSearchResultsDisplayProps> = ({ query, results }) => {
const CodebaseSearchResultsDisplay: React.FC<CodebaseSearchResultsDisplayProps> = ({ results }) => {
const [codebaseSearchResultsExpanded, setCodebaseSearchResultsExpanded] = useState(false)
return (
<div className="flex flex-col gap-2">
<div className="flex flex-col -mt-4 gap-1">
<div
onClick={() => setCodebaseSearchResultsExpanded(!codebaseSearchResultsExpanded)}
className="font-bold cursor-pointer flex items-center justify-between px-2 py-2 rounded border bg-[var(--vscode-editor-background)] border-[var(--vscode-editorGroup-border)]">
className="cursor-pointer flex items-center justify-between px-2 py-2 border bg-[var(--vscode-editor-background)] border-[var(--vscode-editorGroup-border)]">
<span>
<Trans
i18nKey="chat:codebaseSearch.didSearch"
components={{ code: <code></code> }}
values={{ query, count: results.length }}
count={results.length}
values={{ count: results.length }}
/>
</span>
<span className={`codicon codicon-chevron-${codebaseSearchResultsExpanded ? "up" : "down"}`}></span>
</div>
{codebaseSearchResultsExpanded && (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
{results.map((result, idx) => (
<CodebaseSearchResult
key={idx}

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

@ -4,7 +4,7 @@ import { McpTool } from "@roo/mcp"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { StandardTooltip } from "@/components/ui"
import { StandardTooltip, ToggleSwitch } from "@/components/ui"
type McpToolRowProps = {
tool: McpTool
@ -16,6 +16,8 @@ type McpToolRowProps = {
const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => {
const { t } = useAppTranslation()
const isToolEnabled = tool.enabledForPrompt ?? true
const handleAlwaysAllowChange = () => {
if (!serverName) return
vscode.postMessage({
@ -46,17 +48,29 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
onClick={(e) => e.stopPropagation()}>
{/* Tool name section */}
<div className="flex items-center min-w-0 flex-1">
<span className="codicon codicon-symbol-method mr-2 flex-shrink-0 text-vscode-symbolIcon-methodForeground"></span>
<span
className={`codicon codicon-symbol-method mr-2 flex-shrink-0 ${
isToolEnabled
? "text-vscode-symbolIcon-methodForeground"
: "text-vscode-descriptionForeground opacity-60"
}`}></span>
<StandardTooltip content={tool.name}>
<span className="font-medium truncate text-vscode-foreground">{tool.name}</span>
<span
className={`font-medium truncate ${
isToolEnabled
? "text-vscode-foreground"
: "text-vscode-descriptionForeground opacity-60"
}`}>
{tool.name}
</span>
</StandardTooltip>
</div>
{/* Controls section */}
{serverName && (
<div className="flex items-center gap-4 flex-shrink-0">
{/* Always Allow checkbox */}
{alwaysAllowMcp && (
{/* Always Allow checkbox - only show when tool is enabled */}
{alwaysAllowMcp && isToolEnabled && (
<VSCodeCheckbox
checked={tool.alwaysAllow}
onChange={handleAlwaysAllowChange}
@ -68,35 +82,31 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
</VSCodeCheckbox>
)}
{/* Enabled eye button - only show in settings context */}
{/* Enabled toggle switch - only show in settings context */}
{!isInChatContext && (
<StandardTooltip content={t("mcp:tool.togglePromptInclusion")}>
<button
role="button"
aria-pressed={tool.enabledForPrompt}
<ToggleSwitch
checked={isToolEnabled}
onChange={handleEnabledForPromptChange}
size="medium"
aria-label={t("mcp:tool.togglePromptInclusion")}
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${
tool.enabledForPrompt
? "text-vscode-foreground"
: "text-vscode-descriptionForeground opacity-60"
}`}
onClick={handleEnabledForPromptChange}
data-tool-prompt-toggle={tool.name}>
<span
className={`codicon ${
tool.enabledForPrompt ? "codicon-eye-closed" : "codicon-eye"
} text-base`}
/>
</button>
data-testid={`tool-prompt-toggle-${tool.name}`}
/>
</StandardTooltip>
)}
</div>
)}
</div>
{tool.description && (
<div className="mt-1 text-xs text-vscode-descriptionForeground opacity-80">{tool.description}</div>
<div
className={`mt-1 text-xs text-vscode-descriptionForeground ${
isToolEnabled ? "opacity-80" : "opacity-40"
}`}>
{tool.description}
</div>
)}
{tool.inputSchema &&
{isToolEnabled &&
tool.inputSchema &&
"properties" in tool.inputSchema &&
Object.keys(tool.inputSchema.properties as Record<string, any>).length > 0 && (
<div className="mt-2 text-xs border border-vscode-panel-border rounded p-2">

View file

@ -22,6 +22,7 @@ import {
DialogTitle,
DialogDescription,
DialogFooter,
ToggleSwitch,
} from "@src/components/ui"
import { buildDocLink } from "@src/utils/docLinks"
@ -295,54 +296,6 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
style={{ marginRight: "8px" }}>
<span className="codicon codicon-refresh" style={{ fontSize: "14px" }}></span>
</Button>
<div
role="switch"
aria-checked={!server.disabled}
tabIndex={0}
style={{
width: "20px",
height: "10px",
backgroundColor: server.disabled
? "var(--vscode-titleBar-inactiveForeground)"
: "var(--vscode-button-background)",
borderRadius: "5px",
position: "relative",
cursor: "pointer",
transition: "background-color 0.2s",
opacity: server.disabled ? 0.4 : 0.8,
}}
onClick={() => {
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
source: server.source || "global",
disabled: !server.disabled,
})
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
source: server.source || "global",
disabled: !server.disabled,
})
}
}}>
<div
style={{
width: "6px",
height: "6px",
backgroundColor: "var(--vscode-titleBar-activeForeground)",
borderRadius: "50%",
position: "absolute",
top: "2px",
left: server.disabled ? "2px" : "12px",
transition: "left 0.2s",
}}
/>
</div>
</div>
<div
style={{
@ -353,6 +306,21 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
marginLeft: "8px",
}}
/>
<div style={{ marginLeft: "8px" }}>
<ToggleSwitch
checked={!server.disabled}
onChange={() => {
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
source: server.source || "global",
disabled: !server.disabled,
})
}}
size="medium"
aria-label={`Toggle ${server.name} server`}
/>
</div>
</div>
{server.status === "connected" ? (

View file

@ -144,40 +144,40 @@ describe("McpToolRow", () => {
expect(screen.getByText("Second parameter")).toBeInTheDocument()
})
it("shows eye button when serverName is provided and not in chat context", () => {
it("shows toggle switch when serverName is provided and not in chat context", () => {
render(<McpToolRow tool={mockTool} serverName="test-server" />)
const eyeButton = screen.getByRole("button", { name: "Toggle prompt inclusion" })
expect(eyeButton).toBeInTheDocument()
const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
expect(toggleSwitch).toBeInTheDocument()
})
it("hides eye button when isInChatContext is true", () => {
it("hides toggle switch when isInChatContext is true", () => {
render(<McpToolRow tool={mockTool} serverName="test-server" isInChatContext={true} />)
const eyeButton = screen.queryByRole("button", { name: "Toggle prompt inclusion" })
expect(eyeButton).not.toBeInTheDocument()
const toggleSwitch = screen.queryByRole("switch", { name: "Toggle prompt inclusion" })
expect(toggleSwitch).not.toBeInTheDocument()
})
it("shows correct eye icon based on enabledForPrompt state", () => {
// Test when enabled (should show eye-closed icon)
it("shows correct toggle switch state based on enabledForPrompt", () => {
// Test when enabled (should be checked)
const { rerender } = render(<McpToolRow tool={mockTool} serverName="test-server" />)
let eyeIcon = screen.getByRole("button", { name: "Toggle prompt inclusion" }).querySelector("span")
expect(eyeIcon).toHaveClass("codicon-eye-closed")
let toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
expect(toggleSwitch).toHaveAttribute("aria-checked", "true")
// Test when disabled (should show eye icon)
// Test when disabled (should not be checked)
const disabledTool = { ...mockTool, enabledForPrompt: false }
rerender(<McpToolRow tool={disabledTool} serverName="test-server" />)
eyeIcon = screen.getByRole("button", { name: "Toggle prompt inclusion" }).querySelector("span")
expect(eyeIcon).toHaveClass("codicon-eye")
toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
expect(toggleSwitch).toHaveAttribute("aria-checked", "false")
})
it("sends message to toggle enabledForPrompt when eye button is clicked", () => {
it("sends message to toggle enabledForPrompt when toggle switch is clicked", () => {
render(<McpToolRow tool={mockTool} serverName="test-server" />)
const eyeButton = screen.getByRole("button", { name: "Toggle prompt inclusion" })
fireEvent.click(eyeButton)
const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
fireEvent.click(toggleSwitch)
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "toggleToolEnabledForPrompt",
@ -187,4 +187,102 @@ describe("McpToolRow", () => {
isEnabled: false,
})
})
it("hides always allow checkbox when tool is disabled", () => {
const disabledTool = { ...mockTool, enabledForPrompt: false }
render(<McpToolRow tool={disabledTool} serverName="test-server" alwaysAllowMcp={true} />)
expect(screen.queryByText("Always allow")).not.toBeInTheDocument()
})
it("shows always allow checkbox when tool is enabled", () => {
const enabledTool = { ...mockTool, enabledForPrompt: true }
render(<McpToolRow tool={enabledTool} serverName="test-server" alwaysAllowMcp={true} />)
expect(screen.getByText("Always allow")).toBeInTheDocument()
})
it("hides parameters section when tool is disabled", () => {
const disabledToolWithSchema = {
...mockTool,
enabledForPrompt: false,
inputSchema: {
type: "object",
properties: {
param1: {
type: "string",
description: "First parameter",
},
},
required: ["param1"],
},
}
render(<McpToolRow tool={disabledToolWithSchema} serverName="test-server" />)
expect(screen.queryByText("Parameters")).not.toBeInTheDocument()
expect(screen.queryByText("param1")).not.toBeInTheDocument()
expect(screen.queryByText("First parameter")).not.toBeInTheDocument()
})
it("shows parameters section when tool is enabled", () => {
const enabledToolWithSchema = {
...mockTool,
enabledForPrompt: true,
inputSchema: {
type: "object",
properties: {
param1: {
type: "string",
description: "First parameter",
},
},
required: ["param1"],
},
}
render(<McpToolRow tool={enabledToolWithSchema} serverName="test-server" />)
expect(screen.getByText("Parameters")).toBeInTheDocument()
expect(screen.getByText("param1")).toBeInTheDocument()
expect(screen.getByText("First parameter")).toBeInTheDocument()
})
it("grays out tool name and description when tool is disabled", () => {
const disabledTool = {
...mockTool,
enabledForPrompt: false,
description: "A disabled tool",
}
render(<McpToolRow tool={disabledTool} serverName="test-server" />)
const toolName = screen.getByText("test-tool")
const toolDescription = screen.getByText("A disabled tool")
// Check that the tool name has the grayed out classes
expect(toolName).toHaveClass("text-vscode-descriptionForeground", "opacity-60")
// Check that the description has reduced opacity
expect(toolDescription).toHaveClass("opacity-40")
})
it("shows normal styling for tool name and description when tool is enabled", () => {
const enabledTool = {
...mockTool,
enabledForPrompt: true,
description: "An enabled tool",
}
render(<McpToolRow tool={enabledTool} serverName="test-server" />)
const toolName = screen.getByText("test-tool")
const toolDescription = screen.getByText("An enabled tool")
// Check that the tool name has normal styling
expect(toolName).toHaveClass("text-vscode-foreground")
expect(toolName).not.toHaveClass("text-vscode-descriptionForeground", "opacity-60")
// Check that the description has normal opacity
expect(toolDescription).toHaveClass("opacity-80")
expect(toolDescription).not.toHaveClass("opacity-40")
})
})

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

@ -0,0 +1,101 @@
import React from "react"
import { render, fireEvent, screen } from "@/utils/test-utils"
import { ToggleSwitch } from "../toggle-switch"
describe("ToggleSwitch", () => {
it("renders with correct initial state", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={true} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
expect(toggle).toBeInTheDocument()
expect(toggle).toHaveAttribute("aria-checked", "true")
expect(toggle).toHaveAttribute("aria-label", "Test toggle")
})
it("renders unchecked state correctly", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
expect(toggle).toHaveAttribute("aria-checked", "false")
})
it("calls onChange when clicked", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
fireEvent.click(toggle)
expect(onChange).toHaveBeenCalledTimes(1)
})
it("calls onChange when Enter key is pressed", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
fireEvent.keyDown(toggle, { key: "Enter" })
expect(onChange).toHaveBeenCalledTimes(1)
})
it("calls onChange when Space key is pressed", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
fireEvent.keyDown(toggle, { key: " " })
expect(onChange).toHaveBeenCalledTimes(1)
})
it("does not call onChange when disabled", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} disabled={true} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
fireEvent.click(toggle)
fireEvent.keyDown(toggle, { key: "Enter" })
expect(onChange).not.toHaveBeenCalled()
})
it("has correct tabIndex when disabled", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} disabled={true} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
expect(toggle).toHaveAttribute("tabindex", "-1")
})
it("renders with custom data-testid", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} data-testid="custom-toggle" />)
const toggle = screen.getByTestId("custom-toggle")
expect(toggle).toBeInTheDocument()
})
it("supports medium size", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} size="medium" aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
expect(toggle).toBeInTheDocument()
// Medium size should be 20px x 10px
expect(toggle).toHaveStyle({ width: "20px", height: "10px" })
})
it("defaults to small size", () => {
const onChange = vi.fn()
render(<ToggleSwitch checked={false} onChange={onChange} aria-label="Test toggle" />)
const toggle = screen.getByRole("switch")
expect(toggle).toBeInTheDocument()
// Small size should be 16px x 8px
expect(toggle).toHaveStyle({ width: "16px", height: "8px" })
})
})

View file

@ -18,3 +18,4 @@ export * from "./select"
export * from "./textarea"
export * from "./tooltip"
export * from "./standard-tooltip"
export * from "./toggle-switch"

View file

@ -0,0 +1,68 @@
import React from "react"
export interface ToggleSwitchProps {
checked: boolean
onChange: () => void
disabled?: boolean
size?: "small" | "medium"
"aria-label"?: string
"data-testid"?: string
}
export const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
checked,
onChange,
disabled = false,
size = "small",
"aria-label": ariaLabel,
"data-testid": dataTestId,
}) => {
const dimensions = size === "small" ? { width: 16, height: 8, dotSize: 4 } : { width: 20, height: 10, dotSize: 6 }
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
if (!disabled) {
onChange()
}
}
}
return (
<div
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
tabIndex={disabled ? -1 : 0}
data-testid={dataTestId}
style={{
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
backgroundColor: checked
? "var(--vscode-button-background)"
: "var(--vscode-titleBar-inactiveForeground)",
borderRadius: `${dimensions.height / 2}px`,
position: "relative",
cursor: disabled ? "not-allowed" : "pointer",
transition: "background-color 0.2s",
opacity: disabled ? 0.4 : checked ? 0.8 : 0.6,
}}
onClick={disabled ? undefined : onChange}
onKeyDown={handleKeyDown}>
<div
style={{
width: `${dimensions.dotSize}px`,
height: `${dimensions.dotSize}px`,
backgroundColor: "var(--vscode-titleBar-activeForeground)",
borderRadius: "50%",
position: "absolute",
top: `${(dimensions.height - dimensions.dotSize) / 2}px`,
left: checked
? `${dimensions.width - dimensions.dotSize - (dimensions.height - dimensions.dotSize) / 2}px`
: `${(dimensions.height - dimensions.dotSize) / 2}px`,
transition: "left 0.2s",
}}
/>
</div>
)
}

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ó.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo vol cercar a la base de codi <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo vol cercar a la base de codi <code>{{query}}</code> a <code>{{path}}</code>:",
"didSearch": "S'han trobat {{count}} resultat(s) per a <code>{{query}}</code>:",
"didSearch_one": "S'ha trobat 1 resultat",
"didSearch_other": "S'han trobat {{count}} resultats",
"resultTooltip": "Puntuació de similitud: {{score}} (fes clic per obrir el fitxer)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo möchte den Codebase nach <code>{{query}}</code> durchsuchen:",
"wantsToSearchWithPath": "Roo möchte den Codebase nach <code>{{query}}</code> in <code>{{path}}</code> durchsuchen:",
"didSearch": "{{count}} Ergebnis(se) für <code>{{query}}</code> gefunden:",
"didSearch_one": "1 Ergebnis gefunden",
"didSearch_other": "{{count}} Ergebnisse gefunden",
"resultTooltip": "Ähnlichkeitswert: {{score}} (klicken zum Öffnen der Datei)"
},
"read-batch": {

View file

@ -123,6 +123,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",
@ -204,7 +205,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo wants to search the codebase for <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo wants to search the codebase for <code>{{query}}</code> in <code>{{path}}</code>:",
"didSearch": "Found {{count}} result(s) for <code>{{query}}</code>:",
"didSearch_one": "Found 1 result",
"didSearch_other": "Found {{count}} results",
"resultTooltip": "Similarity score: {{score}} (click to open file)"
},
"commandOutput": "Command Output",
@ -296,7 +298,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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo quiere buscar en la base de código <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo quiere buscar en la base de código <code>{{query}}</code> en <code>{{path}}</code>:",
"didSearch": "Se encontraron {{count}} resultado(s) para <code>{{query}}</code>:",
"didSearch_one": "Se encontró 1 resultado",
"didSearch_other": "Se encontraron {{count}} resultados",
"resultTooltip": "Puntuación de similitud: {{score}} (haz clic para abrir el archivo)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo veut rechercher dans la base de code <code>{{query}}</code> :",
"wantsToSearchWithPath": "Roo veut rechercher dans la base de code <code>{{query}}</code> dans <code>{{path}}</code> :",
"didSearch": "{{count}} résultat(s) trouvé(s) pour <code>{{query}}</code> :",
"didSearch_one": "1 résultat trouvé",
"didSearch_other": "{{count}} résultats trouvés",
"resultTooltip": "Score de similarité : {{score}} (cliquer pour ouvrir le fichier)"
},
"read-batch": {

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 टर्मिनल इंटीग्रेशन की समस्या का समाधान कर सकते हैं।",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo कोडबेस में <code>{{query}}</code> खोजना चाहता है:",
"wantsToSearchWithPath": "Roo <code>{{path}}</code> में कोडबेस में <code>{{query}}</code> खोजना चाहता है:",
"didSearch": "<code>{{query}}</code> के लिए {{count}} परिणाम मिले:",
"didSearch_one": "1 परिणाम मिला",
"didSearch_other": "{{count}} परिणाम मिले",
"resultTooltip": "समानता स्कोर: {{score}} (फ़ाइल खोलने के लिए क्लिक करें)"
},
"read-batch": {

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",
@ -207,7 +208,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo ingin mencari codebase untuk <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo ingin mencari codebase untuk <code>{{query}}</code> di <code>{{path}}</code>:",
"didSearch": "Ditemukan {{count}} hasil untuk <code>{{query}}</code>:",
"didSearch_one": "Ditemukan 1 hasil",
"didSearch_other": "Ditemukan {{count}} hasil",
"resultTooltip": "Skor kemiripan: {{score}} (klik untuk membuka file)"
},
"commandOutput": "Output Perintah",
@ -300,7 +302,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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo vuole cercare nella base di codice <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo vuole cercare nella base di codice <code>{{query}}</code> in <code>{{path}}</code>:",
"didSearch": "Trovato {{count}} risultato/i per <code>{{query}}</code>:",
"didSearch_one": "Trovato 1 risultato",
"didSearch_other": "Trovati {{count}} risultati",
"resultTooltip": "Punteggio di somiglianza: {{score}} (clicca per aprire il file)"
},
"read-batch": {

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ターミナル統合のトラブルシューティングを行ってください。",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Rooはコードベースで <code>{{query}}</code> を検索したい:",
"wantsToSearchWithPath": "Rooは <code>{{path}}</code> 内のコードベースで <code>{{query}}</code> を検索したい:",
"didSearch": "<code>{{query}}</code> の検索結果: {{count}} 件",
"didSearch_one": "1件の結果が見つかりました",
"didSearch_other": "{{count}}件の結果が見つかりました",
"resultTooltip": "類似度スコア: {{score}} (クリックしてファイルを開く)"
},
"read-batch": {

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 터미널 통합 문제를 해결하세요.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo가 코드베이스에서 <code>{{query}}</code>을(를) 검색하고 싶어합니다:",
"wantsToSearchWithPath": "Roo가 <code>{{path}}</code>에서 <code>{{query}}</code>을(를) 검색하고 싶어합니다:",
"didSearch": "<code>{{query}}</code>에 대한 검색 결과 {{count}}개 찾음:",
"didSearch_one": "1개의 결과를 찾았습니다",
"didSearch_other": "{{count}}개의 결과를 찾았습니다",
"resultTooltip": "유사도 점수: {{score}} (클릭하여 파일 열기)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo wil de codebase doorzoeken op <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo wil de codebase doorzoeken op <code>{{query}}</code> in <code>{{path}}</code>:",
"didSearch": "{{count}} resultaat/resultaten gevonden voor <code>{{query}}</code>:",
"didSearch_one": "1 resultaat gevonden",
"didSearch_other": "{{count}} resultaten gevonden",
"resultTooltip": "Gelijkenisscore: {{score}} (klik om bestand te openen)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo chce przeszukać bazę kodu w poszukiwaniu <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo chce przeszukać bazę kodu w poszukiwaniu <code>{{query}}</code> w <code>{{path}}</code>:",
"didSearch": "Znaleziono {{count}} wynik(ów) dla <code>{{query}}</code>:",
"didSearch_one": "Znaleziono 1 wynik",
"didSearch_other": "Znaleziono {{count}} wyników",
"resultTooltip": "Wynik podobieństwa: {{score}} (kliknij, aby otworzyć plik)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo quer pesquisar na base de código por <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo quer pesquisar na base de código por <code>{{query}}</code> em <code>{{path}}</code>:",
"didSearch": "Encontrado {{count}} resultado(s) para <code>{{query}}</code>:",
"didSearch_one": "Encontrado 1 resultado",
"didSearch_other": "Encontrados {{count}} resultados",
"resultTooltip": "Pontuação de similaridade: {{score}} (clique para abrir o arquivo)"
},
"read-batch": {

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, используя ссылку ниже.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo хочет выполнить поиск в кодовой базе по <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo хочет выполнить поиск в кодовой базе по <code>{{query}}</code> в <code>{{path}}</code>:",
"didSearch": "Найдено {{count}} результат(ов) для <code>{{query}}</code>:",
"didSearch_one": "Найден 1 результат",
"didSearch_other": "Найдено {{count}} результатов",
"resultTooltip": "Оценка схожести: {{score}} (нажмите, чтобы открыть файл)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo kod tabanında <code>{{query}}</code> aramak istiyor:",
"wantsToSearchWithPath": "Roo <code>{{path}}</code> içinde kod tabanında <code>{{query}}</code> aramak istiyor:",
"didSearch": "<code>{{query}}</code> için {{count}} sonuç bulundu:",
"didSearch_one": "1 sonuç bulundu",
"didSearch_other": "{{count}} sonuç bulundu",
"resultTooltip": "Benzerlik puanı: {{score}} (dosyayı açmak için tıklayın)"
},
"read-batch": {

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.",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo muốn tìm kiếm trong cơ sở mã cho <code>{{query}}</code>:",
"wantsToSearchWithPath": "Roo muốn tìm kiếm trong cơ sở mã cho <code>{{query}}</code> trong <code>{{path}}</code>:",
"didSearch": "Đã tìm thấy {{count}} kết quả cho <code>{{query}}</code>:",
"didSearch_one": "Đã tìm thấy 1 kết quả",
"didSearch_other": "Đã tìm thấy {{count}} kết quả",
"resultTooltip": "Điểm tương tự: {{score}} (nhấp để mở tệp)"
},
"read-batch": {

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 终端集成问题。",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo 需要搜索代码库: <code>{{query}}</code>",
"wantsToSearchWithPath": "Roo 需要在 <code>{{path}}</code> 中搜索: <code>{{query}}</code>",
"didSearch": "找到 {{count}} 个结果: <code>{{query}}</code>",
"didSearch_one": "找到 1 个结果",
"didSearch_other": "找到 {{count}} 个结果",
"resultTooltip": "相似度评分: {{score}} (点击打开文件)"
},
"read-batch": {

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 終端機整合問題。",
@ -296,7 +297,8 @@
"codebaseSearch": {
"wantsToSearch": "Roo 想要搜尋程式碼庫:<code>{{query}}</code>",
"wantsToSearchWithPath": "Roo 想要在 <code>{{path}}</code> 中搜尋:<code>{{query}}</code>",
"didSearch": "找到 {{count}} 個結果:<code>{{query}}</code>",
"didSearch_one": "找到 1 個結果",
"didSearch_other": "找到 {{count}} 個結果",
"resultTooltip": "相似度評分:{{score}} (點擊開啟檔案)"
},
"read-batch": {

View file

@ -28,7 +28,7 @@ class VSCodeAPIWrapper {
* @remarks When running webview code inside a web browser, postMessage will instead
* log the given message to the console.
*
* @param message Abitrary data (must be JSON serializable) to send to the extension context.
* @param message Arbitrary data (must be JSON serializable) to send to the extension context.
*/
public postMessage(message: WebviewMessage) {
if (this.vsCodeApi) {