Merge branch 'RooCodeInc:main' into main

This commit is contained in:
Murilo Pires 2025-07-08 12:46:31 -03:00 committed by GitHub
commit 00a0b631f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 3351 additions and 330 deletions

View file

@ -313,25 +313,6 @@ describe("getEnvironmentDetails", () => {
expect(mockInactiveTerminal.getCurrentWorkingDirectory).toHaveBeenCalled()
})
it("should include warning when file writing is not allowed", async () => {
;(isToolAllowedForMode as Mock).mockReturnValue(false)
;(getModeBySlug as Mock).mockImplementation((slug: string) => {
if (slug === "code") {
return { name: "💻 Code" }
}
if (slug === defaultModeSlug) {
return { name: "Default Mode" }
}
return null
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("NOTE: You are currently in '💻 Code' mode, which does not allow write operations")
})
it("should include experiment-specific details when Power Steering is enabled", async () => {
mockState.experiments = { [EXPERIMENT_IDS.POWER_STEERING]: true }
;(experiments.isEnabled as Mock).mockReturnValue(true)

View file

@ -233,16 +233,6 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
}
}
// Add warning if not in code mode.
if (
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { apply_diff: cline.diffEnabled }) &&
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: cline.diffEnabled })
) {
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode
const defaultModeName = getModeBySlug(defaultModeSlug, customModes)?.name ?? defaultModeSlug
details += `\n\nNOTE: You are currently in '${currentModeName}' mode, which does not allow write operations. To write files, the user will need to switch to a mode that supports file writing, such as '${defaultModeName}' mode.`
}
if (includeFileDetails) {
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))

File diff suppressed because it is too large Load diff

View file

@ -6,9 +6,16 @@ import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import * as yaml from "yaml"
import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from "@roo-code/types"
import {
type Language,
type ProviderSettings,
type GlobalState,
type ClineMessage,
TelemetryEventName,
} from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
import { TelemetryService } from "@roo-code/telemetry"
import { type ApiMessage } from "../task-persistence/apiMessages"
import { ClineProvider } from "./ClineProvider"
import { changeLanguage, t } from "../../i18n"
@ -58,6 +65,200 @@ export const webviewMessageHandler = async (
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
await provider.contextProxy.setValue(key, value)
/**
* Shared utility to find message indices based on timestamp
*/
const findMessageIndices = (messageTs: number, currentCline: any) => {
const timeCutoff = messageTs - 1000 // 1 second buffer before the message
const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts && msg.ts >= timeCutoff)
const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex(
(msg: ApiMessage) => msg.ts && msg.ts >= timeCutoff,
)
return { messageIndex, apiConversationHistoryIndex }
}
/**
* Removes just the target message, preserving messages after the next user message
*/
const removeMessagesJustThis = async (
currentCline: any,
messageIndex: number,
apiConversationHistoryIndex: number,
) => {
// Find the next user message first
const nextUserMessage = currentCline.clineMessages
.slice(messageIndex + 1)
.find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback")
// Handle UI messages
if (nextUserMessage) {
// Find absolute index of next user message
const nextUserMessageIndex = currentCline.clineMessages.findIndex(
(msg: ClineMessage) => msg === nextUserMessage,
)
// Keep messages before current message and after next user message
await currentCline.overwriteClineMessages([
...currentCline.clineMessages.slice(0, messageIndex),
...currentCline.clineMessages.slice(nextUserMessageIndex),
])
} else {
// If no next user message, keep only messages before current message
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
}
// Handle API messages
if (apiConversationHistoryIndex !== -1) {
if (nextUserMessage && nextUserMessage.ts) {
// Keep messages before current API message and after next user message
await currentCline.overwriteApiConversationHistory([
...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
...currentCline.apiConversationHistory.filter(
(msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts,
),
])
} else {
// If no next user message, keep only messages before current API message
await currentCline.overwriteApiConversationHistory(
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}
}
}
/**
* Removes the target message and all subsequent messages
*/
const removeMessagesThisAndSubsequent = async (
currentCline: any,
messageIndex: number,
apiConversationHistoryIndex: number,
) => {
// Delete this message and all that follow
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
if (apiConversationHistoryIndex !== -1) {
await currentCline.overwriteApiConversationHistory(
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}
}
/**
* Handles message deletion operations with user confirmation
*/
const handleDeleteOperation = async (messageTs: number): Promise<void> => {
const options = [
t("common:confirmation.delete_just_this_message"),
t("common:confirmation.delete_this_and_subsequent"),
]
const answer = await vscode.window.showInformationMessage(
t("common:confirmation.delete_message"),
{ modal: true },
...options,
)
// Only proceed if user selected one of the options and we have a current cline
if (answer && options.includes(answer) && provider.getCurrentCline()) {
const currentCline = provider.getCurrentCline()!
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
if (messageIndex !== -1) {
try {
const { historyItem } = await provider.getTaskWithId(currentCline.taskId)
// Check which option the user selected
if (answer === options[0]) {
// Delete just this message
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
} else if (answer === options[1]) {
// Delete this message and all subsequent
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
}
// Initialize with history item after deletion
await provider.initClineWithHistoryItem(historyItem)
} catch (error) {
console.error("Error in delete message:", error)
vscode.window.showErrorMessage(
`Error deleting message: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
}
}
/**
* Handles message editing operations with user confirmation
*/
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
const options = [
t("common:confirmation.edit_this_and_delete_subsequent"),
t("common:confirmation.edit_just_this_message"),
]
const answer = await vscode.window.showInformationMessage(
t("common:confirmation.edit_message"),
{ modal: true },
...options,
)
// Only proceed if user selected one of the options and we have a current cline
if (answer && options.includes(answer) && provider.getCurrentCline()) {
const currentCline = provider.getCurrentCline()!
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
if (messageIndex !== -1) {
try {
// Check which option the user selected
if (answer === options[0]) {
// Edit this message and delete subsequent
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
} else if (answer === options[1]) {
// Edit just this message
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
}
// Process the edited message as a regular user message
// This will add it to the conversation and trigger an AI response
webviewMessageHandler(provider, {
type: "askResponse",
askResponse: "messageResponse",
text: editedContent,
})
// Don't initialize with history item for edit operations
// The webviewMessageHandler will handle the conversation state
} catch (error) {
console.error("Error in edit message:", error)
vscode.window.showErrorMessage(
`Error editing message: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
}
}
/**
* Handles message modification operations (delete or edit) with confirmation dialog
* @param messageTs Timestamp of the message to operate on
* @param operation Type of operation ('delete' or 'edit')
* @param editedContent New content for edit operations
* @returns Promise<void>
*/
const handleMessageModificationsOperation = async (
messageTs: number,
operation: "delete" | "edit",
editedContent?: string,
): Promise<void> => {
if (operation === "delete") {
await handleDeleteOperation(messageTs)
} else if (operation === "edit" && editedContent) {
await handleEditOperation(messageTs, editedContent)
}
}
switch (message.type) {
case "webviewDidLaunch":
// Load custom modes first
@ -989,108 +1190,19 @@ export const webviewMessageHandler = async (
}
break
case "deleteMessage": {
const answer = await vscode.window.showInformationMessage(
t("common:confirmation.delete_message"),
{ modal: true },
t("common:confirmation.just_this_message"),
t("common:confirmation.this_and_subsequent"),
)
if (provider.getCurrentCline() && typeof message.value === "number" && message.value) {
await handleMessageModificationsOperation(message.value, "delete")
}
break
}
case "submitEditedMessage": {
if (
(answer === t("common:confirmation.just_this_message") ||
answer === t("common:confirmation.this_and_subsequent")) &&
provider.getCurrentCline() &&
typeof message.value === "number" &&
message.value
message.value &&
message.editedMessageContent
) {
const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
const messageIndex = provider
.getCurrentCline()!
.clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
const apiConversationHistoryIndex = provider
.getCurrentCline()
?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
if (messageIndex !== -1) {
const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId)
if (answer === t("common:confirmation.just_this_message")) {
// Find the next user message first
const nextUserMessage = provider
.getCurrentCline()!
.clineMessages.slice(messageIndex + 1)
.find((msg) => msg.type === "say" && msg.say === "user_feedback")
// Handle UI messages
if (nextUserMessage) {
// Find absolute index of next user message
const nextUserMessageIndex = provider
.getCurrentCline()!
.clineMessages.findIndex((msg) => msg === nextUserMessage)
// Keep messages before current message and after next user message
await provider
.getCurrentCline()!
.overwriteClineMessages([
...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
])
} else {
// If no next user message, keep only messages before current message
await provider
.getCurrentCline()!
.overwriteClineMessages(
provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
)
}
// Handle API messages
if (apiConversationHistoryIndex !== -1) {
if (nextUserMessage && nextUserMessage.ts) {
// Keep messages before current API message and after next user message
await provider
.getCurrentCline()!
.overwriteApiConversationHistory([
...provider
.getCurrentCline()!
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
...provider
.getCurrentCline()!
.apiConversationHistory.filter(
(msg) => msg.ts && msg.ts >= nextUserMessage.ts,
),
])
} else {
// If no next user message, keep only messages before current API message
await provider
.getCurrentCline()!
.overwriteApiConversationHistory(
provider
.getCurrentCline()!
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}
}
} else if (answer === t("common:confirmation.this_and_subsequent")) {
// Delete this message and all that follow
await provider
.getCurrentCline()!
.overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex))
if (apiConversationHistoryIndex !== -1) {
await provider
.getCurrentCline()!
.overwriteApiConversationHistory(
provider
.getCurrentCline()!
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}
}
await provider.initClineWithHistoryItem(historyItem)
}
await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent)
}
break
}
@ -1843,8 +1955,12 @@ export const webviewMessageHandler = async (
const settings = message.codeIndexSettings
try {
// Save global state settings atomically (without codebaseIndexEnabled which is now in global settings)
// Check if embedder provider has changed
const currentConfig = getGlobalState("codebaseIndexConfig") || {}
const embedderProviderChanged =
currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
// Save global state settings atomically (without codebaseIndexEnabled which is now in global settings)
const globalStateConfig = {
...currentConfig,
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
@ -1880,23 +1996,7 @@ export const webviewMessageHandler = async (
)
}
// Verify secrets are actually stored
const storedOpenAiKey = provider.contextProxy.getSecret("codeIndexOpenAiKey")
// Notify code index manager of changes
if (provider.codeIndexManager) {
await provider.codeIndexManager.handleSettingsChange()
// Auto-start indexing if now enabled and configured
if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {
if (!provider.codeIndexManager.isInitialized) {
await provider.codeIndexManager.initialize(provider.contextProxy)
}
provider.codeIndexManager.startIndexing()
}
}
// Send success response
// Send success response first - settings are saved regardless of validation
await provider.postMessageToWebview({
type: "codeIndexSettingsSaved",
success: true,
@ -1905,6 +2005,61 @@ export const webviewMessageHandler = async (
// Update webview state
await provider.postStateToWebview()
// Then handle validation and initialization
if (provider.codeIndexManager) {
// If embedder provider changed, perform proactive validation
if (embedderProviderChanged) {
try {
// Force handleSettingsChange which will trigger validation
await provider.codeIndexManager.handleSettingsChange()
} catch (error) {
// Validation failed - the error state is already set by handleSettingsChange
provider.log(
`Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`,
)
// Send validation error to webview
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: provider.codeIndexManager.getCurrentStatus(),
})
// Exit early - don't try to start indexing with invalid configuration
break
}
} else {
// No provider change, just handle settings normally
try {
await provider.codeIndexManager.handleSettingsChange()
} catch (error) {
// Log but don't fail - settings are saved
provider.log(
`Settings change handling error: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// Wait a bit more to ensure everything is ready
await new Promise((resolve) => setTimeout(resolve, 200))
// Auto-start indexing if now enabled and configured
if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {
if (!provider.codeIndexManager.isInitialized) {
try {
await provider.codeIndexManager.initialize(provider.contextProxy)
provider.log(`Code index manager initialized after settings save`)
} catch (error) {
provider.log(
`Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`,
)
// Send error status to webview
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: provider.codeIndexManager.getCurrentStatus(),
})
}
}
}
}
} catch (error) {
provider.log(`Error saving code index settings: ${error.message || error}`)
await provider.postMessageToWebview({

View file

@ -23,8 +23,11 @@
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}",
"delete_message": "Què vols eliminar?",
"just_this_message": "Només aquest missatge",
"this_and_subsequent": "Aquest i tots els missatges posteriors"
"edit_message": "Eliminar tots els missatges després d'aquest?",
"delete_just_this_message": "Només aquest missatge",
"edit_just_this_message": "No, només editar aquest",
"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
"edit_this_and_delete_subsequent": "Sí"
},
"errors": {
"invalid_data_uri": "Format d'URI de dades no vàlid",
@ -112,6 +115,11 @@
"remove": "Eliminar",
"keep": "Mantenir"
},
"buttons": {
"save": "Desar",
"cancel": "Cancel·lar",
"edit": "Editar"
},
"tasks": {
"canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.",
"deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "No s'ha pogut llegir el cos de l'error",
"requestFailed": "La sol·licitud de l'API d'Ollama ha fallat amb l'estat {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Estructura de resposta no vàlida de l'API d'Ollama: no s'ha trobat la matriu \"embeddings\" o no és una matriu.",
"embeddingFailed": "La incrustació d'Ollama ha fallat: {{message}}"
"embeddingFailed": "La incrustació d'Ollama ha fallat: {{message}}",
"serviceNotRunning": "El servei d'Ollama no s'està executant a {{baseUrl}}",
"serviceUnavailable": "El servei d'Ollama no està disponible (estat: {{status}})",
"modelNotFound": "No s'ha trobat el model d'Ollama: {{modelId}}",
"modelNotEmbeddingCapable": "El model d'Ollama no és capaç de fer incrustacions: {{modelId}}",
"hostNotFound": "No s'ha trobat l'amfitrió d'Ollama: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Error desconegut en processar el fitxer {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Ha fallat l'autenticació. Comproveu la vostra clau d'API a la configuració.",
"connectionFailed": "No s'ha pogut connectar al servei d'incrustació. Comproveu la vostra configuració de connexió i assegureu-vos que el servei estigui funcionant.",
"modelNotAvailable": "El model especificat no està disponible. Comproveu la vostra configuració de model.",
"configurationError": "Configuració d'incrustació no vàlida. Reviseu la vostra configuració.",
"serviceUnavailable": "El servei d'incrustació no està disponible. Assegureu-vos que estigui funcionant i sigui accessible.",
"invalidEndpoint": "Punt final d'API no vàlid. Comproveu la vostra configuració d'URL.",
"invalidEmbedderConfig": "Configuració d'incrustació no vàlida. Comproveu la vostra configuració.",
"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ó."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}",
"delete_message": "Was möchtest du löschen?",
"just_this_message": "Nur diese Nachricht",
"this_and_subsequent": "Diese und alle nachfolgenden Nachrichten"
"edit_message": "Alle Nachrichten nach dieser löschen?",
"delete_just_this_message": "Nur diese Nachricht",
"edit_just_this_message": "Nein, nur diese bearbeiten",
"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
"edit_this_and_delete_subsequent": "Ja"
},
"errors": {
"invalid_data_uri": "Ungültiges Daten-URI-Format",
@ -108,6 +111,11 @@
"remove": "Entfernen",
"keep": "Behalten"
},
"buttons": {
"save": "Speichern",
"cancel": "Abbrechen",
"edit": "Bearbeiten"
},
"tasks": {
"canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.",
"deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Fehlerinhalt konnte nicht gelesen werden",
"requestFailed": "Ollama API-Anfrage fehlgeschlagen mit Status {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Ungültige Antwortstruktur von Ollama API: \"embeddings\" Array nicht gefunden oder kein Array.",
"embeddingFailed": "Ollama Einbettung fehlgeschlagen: {{message}}"
"embeddingFailed": "Ollama Einbettung fehlgeschlagen: {{message}}",
"serviceNotRunning": "Ollama-Dienst wird unter {{baseUrl}} nicht ausgeführt",
"serviceUnavailable": "Ollama-Dienst ist nicht verfügbar (Status: {{status}})",
"modelNotFound": "Ollama-Modell nicht gefunden: {{modelId}}",
"modelNotEmbeddingCapable": "Ollama-Modell ist nicht für Einbettungen geeignet: {{modelId}}",
"hostNotFound": "Ollama-Host nicht gefunden: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Unbekannter Fehler beim Verarbeiten der Datei {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Verbindung zur Qdrant-Vektordatenbank fehlgeschlagen. Stelle sicher, dass Qdrant läuft und unter {{qdrantUrl}} erreichbar ist. Fehler: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfe deinen API-Schlüssel in den Einstellungen.",
"connectionFailed": "Verbindung zum Embedder-Dienst fehlgeschlagen. Bitte überprüfe deine Verbindungseinstellungen und stelle sicher, dass der Dienst läuft.",
"modelNotAvailable": "Das angegebene Modell ist nicht verfügbar. Bitte überprüfe deine Modellkonfiguration.",
"configurationError": "Ungültige Embedder-Konfiguration. Bitte überprüfe deine Einstellungen.",
"serviceUnavailable": "Der Embedder-Dienst ist nicht verfügbar. Bitte stelle sicher, dass er läuft und erreichbar ist.",
"invalidEndpoint": "Ungültiger API-Endpunkt. Bitte überprüfe deine URL-Konfiguration.",
"invalidEmbedderConfig": "Ungültige Embedder-Konfiguration. Bitte überprüfe deine Einstellungen.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}",
"delete_message": "What would you like to delete?",
"just_this_message": "Just this message",
"this_and_subsequent": "This and all subsequent messages"
"edit_message": "Delete all messages after this one?",
"delete_just_this_message": "Just this message",
"edit_just_this_message": "No, just edit this one",
"delete_this_and_subsequent": "This and all subsequent messages",
"edit_this_and_delete_subsequent": "Yes"
},
"errors": {
"invalid_data_uri": "Invalid data URI format",
@ -108,6 +111,11 @@
"remove": "Remove",
"keep": "Keep"
},
"buttons": {
"save": "Save",
"cancel": "Cancel",
"edit": "Edit"
},
"tasks": {
"canceled": "Task error: It was stopped and canceled by the user.",
"deleted": "Task failure: It was stopped and deleted by the user.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Could not read error body",
"requestFailed": "Ollama API request failed with status {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Invalid response structure from Ollama API: \"embeddings\" array not found or not an array.",
"embeddingFailed": "Ollama embedding failed: {{message}}"
"embeddingFailed": "Ollama embedding failed: {{message}}",
"serviceNotRunning": "Ollama service is not running at {{baseUrl}}",
"serviceUnavailable": "Ollama service is unavailable (status: {{status}})",
"modelNotFound": "Ollama model not found: {{modelId}}",
"modelNotEmbeddingCapable": "Ollama model is not embedding capable: {{modelId}}",
"hostNotFound": "Ollama host not found: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Unknown error processing file {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authentication failed. Please check your API key in the settings.",
"connectionFailed": "Failed to connect to the embedder service. Please check your connection settings and ensure the service is running.",
"modelNotAvailable": "The specified model is not available. Please check your model configuration.",
"configurationError": "Invalid embedder configuration. Please review your settings.",
"serviceUnavailable": "The embedder service is not available. Please ensure it is running and accessible.",
"invalidEndpoint": "Invalid API endpoint. Please check your URL configuration.",
"invalidEmbedderConfig": "Invalid embedder configuration. Please check your settings.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}",
"delete_message": "¿Qué deseas eliminar?",
"just_this_message": "Solo este mensaje",
"this_and_subsequent": "Este y todos los mensajes posteriores"
"edit_message": "¿Eliminar todos los mensajes posteriores a este?",
"delete_just_this_message": "Solo este mensaje",
"edit_just_this_message": "No, solo editar este",
"delete_this_and_subsequent": "Este y todos los mensajes posteriores",
"edit_this_and_delete_subsequent": "Sí"
},
"errors": {
"invalid_data_uri": "Formato de URI de datos no válido",
@ -108,6 +111,11 @@
"remove": "Eliminar",
"keep": "Mantener"
},
"buttons": {
"save": "Guardar",
"cancel": "Cancelar",
"edit": "Editar"
},
"tasks": {
"canceled": "Error de tarea: Fue detenida y cancelada por el usuario.",
"deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "No se pudo leer el cuerpo del error",
"requestFailed": "La solicitud de la API de Ollama falló con estado {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Estructura de respuesta inválida de la API de Ollama: array \"embeddings\" no encontrado o no es un array.",
"embeddingFailed": "Incrustación de Ollama falló: {{message}}"
"embeddingFailed": "Incrustación de Ollama falló: {{message}}",
"serviceNotRunning": "El servicio Ollama no se está ejecutando en {{baseUrl}}",
"serviceUnavailable": "El servicio Ollama no está disponible (estado: {{status}})",
"modelNotFound": "No se encuentra el modelo Ollama: {{modelId}}",
"modelNotEmbeddingCapable": "El modelo Ollama no es capaz de realizar incrustaciones: {{modelId}}",
"hostNotFound": "No se encuentra el host de Ollama: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Error desconocido procesando archivo {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Error de autenticación. Comprueba tu clave de API en los ajustes.",
"connectionFailed": "Error al conectar con el servicio de embedder. Comprueba los ajustes de conexión y asegúrate de que el servicio esté funcionando.",
"modelNotAvailable": "El modelo especificado no está disponible. Comprueba la configuración de tu modelo.",
"configurationError": "Configuración de embedder no válida. Revisa tus ajustes.",
"serviceUnavailable": "El servicio de embedder no está disponible. Asegúrate de que esté funcionando y sea accesible.",
"invalidEndpoint": "Punto de conexión de API no válido. Comprueba la configuración de tu URL.",
"invalidEmbedderConfig": "Configuración de embedder no válida. Comprueba tus ajustes.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?",
"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}",
"delete_message": "Que souhaitez-vous supprimer ?",
"just_this_message": "Uniquement ce message",
"this_and_subsequent": "Ce message et tous les messages suivants"
"edit_message": "Supprimer tous les messages après celui-ci ?",
"delete_just_this_message": "Uniquement ce message",
"edit_just_this_message": "Non, modifier uniquement celui-ci",
"delete_this_and_subsequent": "Ce message et tous les messages suivants",
"edit_this_and_delete_subsequent": "Oui"
},
"errors": {
"invalid_data_uri": "Format d'URI de données invalide",
@ -108,6 +111,11 @@
"remove": "Supprimer",
"keep": "Conserver"
},
"buttons": {
"save": "Enregistrer",
"cancel": "Annuler",
"edit": "Modifier"
},
"tasks": {
"canceled": "Erreur de tâche : Elle a été arrêtée et annulée par l'utilisateur.",
"deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Impossible de lire le corps de l'erreur",
"requestFailed": "Échec de la requête API Ollama avec le statut {{status}} {{statusText}} : {{errorBody}}",
"invalidResponseStructure": "Structure de réponse invalide de l'API Ollama : tableau \"embeddings\" non trouvé ou n'est pas un tableau.",
"embeddingFailed": "Échec de l'embedding Ollama : {{message}}"
"embeddingFailed": "Échec de l'embedding Ollama : {{message}}",
"serviceNotRunning": "Le service Ollama n'est pas en cours d'exécution sur {{baseUrl}}",
"serviceUnavailable": "Le service Ollama est indisponible (statut : {{status}})",
"modelNotFound": "Modèle Ollama introuvable : {{modelId}}",
"modelNotEmbeddingCapable": "Le modèle Ollama n'est pas capable d'intégrer : {{modelId}}",
"hostNotFound": "Hôte Ollama introuvable : {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Erreur inconnue lors du traitement du fichier {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Échec de l'authentification. Veuillez vérifier votre clé API dans les paramètres.",
"connectionFailed": "Échec de la connexion au service d'embedding. Veuillez vérifier vos paramètres de connexion et vous assurer que le service est en cours d'exécution.",
"modelNotAvailable": "Le modèle spécifié n'est pas disponible. Veuillez vérifier la configuration de votre modèle.",
"configurationError": "Configuration de l'embedder invalide. Veuillez vérifier vos paramètres.",
"serviceUnavailable": "Le service d'embedding n'est pas disponible. Veuillez vous assurer qu'il est en cours d'exécution et accessible.",
"invalidEndpoint": "Point de terminaison d'API invalide. Veuillez vérifier votre configuration d'URL.",
"invalidEmbedderConfig": "Configuration de l'embedder invalide. Veuillez vérifier vos paramètres.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?",
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}",
"delete_message": "आप क्या हटाना चाहते हैं?",
"just_this_message": "सिर्फ यह संदेश",
"this_and_subsequent": "यह और सभी बाद के संदेश"
"edit_message": "इसके बाद के सभी संदेशों को हटाएं?",
"delete_just_this_message": "सिर्फ यह संदेश",
"edit_just_this_message": "नहीं, केवल इसे संपादित करें",
"delete_this_and_subsequent": "यह और सभी बाद के संदेश",
"edit_this_and_delete_subsequent": "हां"
},
"errors": {
"invalid_data_uri": "अमान्य डेटा URI फॉर्मेट",
@ -108,6 +111,11 @@
"remove": "हटाएं",
"keep": "रखें"
},
"buttons": {
"save": "सहेजें",
"cancel": "रद्द करें",
"edit": "संपादित करें"
},
"tasks": {
"canceled": "टास्क त्रुटि: इसे उपयोगकर्ता द्वारा रोका और रद्द किया गया था।",
"deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "त्रुटि सामग्री पढ़ नहीं सका",
"requestFailed": "Ollama API अनुरोध स्थिति {{status}} {{statusText}} के साथ विफल: {{errorBody}}",
"invalidResponseStructure": "Ollama API से अमान्य प्रतिक्रिया संरचना: \"embeddings\" सरणी नहीं मिली या सरणी नहीं है।",
"embeddingFailed": "Ollama एम्बेडिंग विफल: {{message}}"
"embeddingFailed": "Ollama एम्बेडिंग विफल: {{message}}",
"serviceNotRunning": "ओलामा सेवा {{baseUrl}} पर नहीं चल रही है",
"serviceUnavailable": "ओलामा सेवा अनुपलब्ध है (स्थिति: {{status}})",
"modelNotFound": "ओलामा मॉडल नहीं मिला: {{modelId}}",
"modelNotEmbeddingCapable": "ओलामा मॉडल एम्बेडिंग में सक्षम नहीं है: {{modelId}}",
"hostNotFound": "ओलामा होस्ट नहीं मिला: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "फ़ाइल {{filePath}} प्रसंस्करण में अज्ञात त्रुटि",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant वेक्टर डेटाबेस से कनेक्ट करने में विफल। कृपया सुनिश्चित करें कि Qdrant चल रहा है और {{qdrantUrl}} पर पहुंच योग्य है। त्रुटि: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "प्रमाणीकरण विफल। कृपया सेटिंग्स में अपनी एपीआई कुंजी जांचें।",
"connectionFailed": "एम्बेडर सेवा से कनेक्ट करने में विफल। कृपया अपनी कनेक्शन सेटिंग्स जांचें और सुनिश्चित करें कि सेवा चल रही है।",
"modelNotAvailable": "निर्दिष्ट मॉडल उपलब्ध नहीं है। कृपया अपनी मॉडल कॉन्फ़िगरेशन जांचें।",
"configurationError": "अमान्य एम्बेडर कॉन्फ़िगरेशन। कृपया अपनी सेटिंग्स की समीक्षा करें।",
"serviceUnavailable": "एम्बेडर सेवा उपलब्ध नहीं है। कृपया सुनिश्चित करें कि यह चल रहा है और पहुंच योग्य है।",
"invalidEndpoint": "अमान्य एपीआई एंडपॉइंट। कृपया अपनी यूआरएल कॉन्फ़िगरेशन जांचें।",
"invalidEmbedderConfig": "अमान्य एम्बेडर कॉन्फ़िगरेशन। कृपया अपनी सेटिंग्स जांचें।",
"invalidApiKey": "अमान्य एपीआई कुंजी। कृपया अपनी एपीआई कुंजी कॉन्फ़िगरेशन जांचें।",
"invalidBaseUrl": "अमान्य बेस यूआरएल। कृपया अपनी यूआरएल कॉन्फ़िगरेशन जांचें।",
"invalidModel": "अमान्य मॉडल। कृपया अपनी मॉडल कॉन्फ़िगरेशन जांचें।",
"invalidResponse": "एम्बेडर सेवा से अमान्य प्रतिक्रिया। कृपया अपनी कॉन्फ़िगरेशन जांचें।"
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?",
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}",
"delete_message": "Apa yang ingin kamu hapus?",
"just_this_message": "Hanya pesan ini",
"this_and_subsequent": "Ini dan semua pesan selanjutnya"
"edit_message": "Hapus semua pesan setelah ini?",
"delete_just_this_message": "Hanya pesan ini",
"edit_just_this_message": "Tidak, hanya edit yang ini",
"delete_this_and_subsequent": "Ini dan semua pesan selanjutnya",
"edit_this_and_delete_subsequent": "Ya"
},
"errors": {
"invalid_data_uri": "Format data URI tidak valid",
@ -108,6 +111,11 @@
"remove": "Hapus",
"keep": "Simpan"
},
"buttons": {
"save": "Simpan",
"cancel": "Batal",
"edit": "Edit"
},
"tasks": {
"canceled": "Error tugas: Dihentikan dan dibatalkan oleh pengguna.",
"deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Tidak dapat membaca body error",
"requestFailed": "Permintaan API Ollama gagal dengan status {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Struktur respons tidak valid dari API Ollama: array \"embeddings\" tidak ditemukan atau bukan array.",
"embeddingFailed": "Embedding Ollama gagal: {{message}}"
"embeddingFailed": "Embedding Ollama gagal: {{message}}",
"serviceNotRunning": "Layanan Ollama tidak berjalan di {{baseUrl}}",
"serviceUnavailable": "Layanan Ollama tidak tersedia (status: {{status}})",
"modelNotFound": "Model Ollama tidak ditemukan: {{modelId}}",
"modelNotEmbeddingCapable": "Model Ollama tidak mampu melakukan embedding: {{modelId}}",
"hostNotFound": "Host Ollama tidak ditemukan: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Error tidak dikenal saat memproses file {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Gagal terhubung ke database vektor Qdrant. Pastikan Qdrant berjalan dan dapat diakses di {{qdrantUrl}}. Error: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Autentikasi gagal. Silakan periksa kunci API Anda di pengaturan.",
"connectionFailed": "Gagal terhubung ke layanan embedder. Silakan periksa pengaturan koneksi Anda dan pastikan layanan berjalan.",
"modelNotAvailable": "Model yang ditentukan tidak tersedia. Silakan periksa konfigurasi model Anda.",
"configurationError": "Konfigurasi embedder tidak valid. Harap tinjau pengaturan Anda.",
"serviceUnavailable": "Layanan embedder tidak tersedia. Pastikan layanan tersebut berjalan dan dapat diakses.",
"invalidEndpoint": "Endpoint API tidak valid. Silakan periksa konfigurasi URL Anda.",
"invalidEmbedderConfig": "Konfigurasi embedder tidak valid. Silakan periksa pengaturan Anda.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?",
"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}",
"delete_message": "Cosa desideri eliminare?",
"just_this_message": "Solo questo messaggio",
"this_and_subsequent": "Questo e tutti i messaggi successivi"
"edit_message": "Eliminare tutti i messaggi dopo questo?",
"delete_just_this_message": "Solo questo messaggio",
"edit_just_this_message": "No, modifica solo questo",
"delete_this_and_subsequent": "Questo e tutti i messaggi successivi",
"edit_this_and_delete_subsequent": "Sì"
},
"errors": {
"invalid_data_uri": "Formato URI dati non valido",
@ -108,6 +111,11 @@
"remove": "Rimuovi",
"keep": "Mantieni"
},
"buttons": {
"save": "Salva",
"cancel": "Annulla",
"edit": "Modifica"
},
"tasks": {
"canceled": "Errore attività: È stata interrotta e annullata dall'utente.",
"deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Impossibile leggere il corpo dell'errore",
"requestFailed": "Richiesta API Ollama fallita con stato {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Struttura di risposta non valida dall'API Ollama: array \"embeddings\" non trovato o non è un array.",
"embeddingFailed": "Embedding Ollama fallito: {{message}}"
"embeddingFailed": "Embedding Ollama fallito: {{message}}",
"serviceNotRunning": "Il servizio Ollama non è in esecuzione su {{baseUrl}}",
"serviceUnavailable": "Il servizio Ollama non è disponibile (stato: {{status}})",
"modelNotFound": "Modello Ollama non trovato: {{modelId}}",
"modelNotEmbeddingCapable": "Il modello Ollama non è in grado di eseguire l'embedding: {{modelId}}",
"hostNotFound": "Host Ollama non trovato: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Errore sconosciuto nell'elaborazione del file {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Impossibile connettersi al database vettoriale Qdrant. Assicurati che Qdrant sia in esecuzione e accessibile su {{qdrantUrl}}. Errore: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Autenticazione fallita. Controlla la tua chiave API nelle impostazioni.",
"connectionFailed": "Connessione al servizio di embedder fallita. Controlla le impostazioni di connessione e assicurati che il servizio sia in esecuzione.",
"modelNotAvailable": "Il modello specificato non è disponibile. Controlla la configurazione del tuo modello.",
"configurationError": "Configurazione dell'embedder non valida. Rivedi le tue impostazioni.",
"serviceUnavailable": "Il servizio di embedder non è disponibile. Assicurati che sia in esecuzione e accessibile.",
"invalidEndpoint": "Endpoint API non valido. Controlla la configurazione del tuo URL.",
"invalidEmbedderConfig": "Configurazione dell'embedder non valida. Controlla le tue impostazioni.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "この設定プロファイルを削除してもよろしいですか?",
"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}",
"delete_message": "何を削除しますか?",
"just_this_message": "このメッセージのみ",
"this_and_subsequent": "これ以降のすべてのメッセージ"
"edit_message": "これ以降のメッセージをすべて削除しますか?",
"delete_just_this_message": "このメッセージのみ",
"edit_just_this_message": "いいえ、これだけを編集",
"delete_this_and_subsequent": "これ以降のすべてのメッセージ",
"edit_this_and_delete_subsequent": "はい"
},
"errors": {
"invalid_data_uri": "データURIフォーマットが無効です",
@ -108,6 +111,11 @@
"remove": "削除",
"keep": "保持"
},
"buttons": {
"save": "保存",
"cancel": "キャンセル",
"edit": "編集"
},
"tasks": {
"canceled": "タスクエラー:ユーザーによって停止およびキャンセルされました。",
"deleted": "タスク失敗:ユーザーによって停止および削除されました。",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "エラー本文を読み取れませんでした",
"requestFailed": "Ollama APIリクエストが失敗しました。ステータス {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Ollama APIからの無効な応答構造\"embeddings\"配列が見つからないか、配列ではありません。",
"embeddingFailed": "Ollama埋め込みが失敗しました{{message}}"
"embeddingFailed": "Ollama埋め込みが失敗しました{{message}}",
"serviceNotRunning": "Ollamaサービスは{{baseUrl}}で実行されていません",
"serviceUnavailable": "Ollamaサービスは利用できませんステータス{{status}}",
"modelNotFound": "Ollamaモデルが見つかりません{{modelId}}",
"modelNotEmbeddingCapable": "Ollamaモデルは埋め込みに対応していません{{modelId}}",
"hostNotFound": "Ollamaホストが見つかりません{{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "ファイル{{filePath}}の処理中に不明なエラーが発生しました",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrantベクターデータベースへの接続に失敗しました。Qdrantが実行中で{{qdrantUrl}}でアクセス可能であることを確認してください。エラー:{{errorMessage}}"
},
"validation": {
"authenticationFailed": "認証に失敗しました。設定でAPIキーを確認してください。",
"connectionFailed": "エンベッダーサービスへの接続に失敗しました。接続設定を確認し、サービスが実行されていることを確認してください。",
"modelNotAvailable": "指定されたモデルは利用できません。モデル構成を確認してください。",
"configurationError": "無効なエンベッダー構成です。設定を確認してください。",
"serviceUnavailable": "エンベッダーサービスは利用できません。実行中でアクセス可能であることを確認してください。",
"invalidEndpoint": "無効なAPIエンドポイントです。URL構成を確認してください。",
"invalidEmbedderConfig": "無効なエンベッダー構成です。設定を確認してください。",
"invalidApiKey": "無効なAPIキーです。APIキー構成を確認してください。",
"invalidBaseUrl": "無効なベースURLです。URL構成を確認してください。",
"invalidModel": "無効なモデルです。モデル構成を確認してください。",
"invalidResponse": "エンベッダーサービスからの無効な応答です。設定を確認してください。"
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?",
"delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}",
"delete_message": "무엇을 삭제하시겠습니까?",
"just_this_message": "이 메시지만",
"this_and_subsequent": "이 메시지와 모든 후속 메시지"
"edit_message": "이 메시지 이후의 모든 메시지를 삭제하시겠습니까?",
"delete_just_this_message": "이 메시지만",
"edit_just_this_message": "아니요, 이것만 편집",
"delete_this_and_subsequent": "이 메시지와 모든 후속 메시지",
"edit_this_and_delete_subsequent": "예"
},
"errors": {
"invalid_data_uri": "잘못된 데이터 URI 형식",
@ -108,6 +111,11 @@
"remove": "제거",
"keep": "유지"
},
"buttons": {
"save": "저장",
"cancel": "취소",
"edit": "편집"
},
"tasks": {
"canceled": "작업 오류: 사용자에 의해 중지 및 취소되었습니다.",
"deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "오류 본문을 읽을 수 없습니다",
"requestFailed": "Ollama API 요청이 실패했습니다. 상태 {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Ollama API에서 잘못된 응답 구조: \"embeddings\" 배열을 찾을 수 없거나 배열이 아닙니다.",
"embeddingFailed": "Ollama 임베딩 실패: {{message}}"
"embeddingFailed": "Ollama 임베딩 실패: {{message}}",
"serviceNotRunning": "Ollama 서비스가 {{baseUrl}}에서 실행되고 있지 않습니다",
"serviceUnavailable": "Ollama 서비스를 사용할 수 없습니다 (상태: {{status}})",
"modelNotFound": "Ollama 모델을 찾을 수 없습니다: {{modelId}}",
"modelNotEmbeddingCapable": "Ollama 모델은 임베딩이 불가능합니다: {{modelId}}",
"hostNotFound": "Ollama 호스트를 찾을 수 없습니다: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "파일 {{filePath}} 처리 중 알 수 없는 오류",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant 벡터 데이터베이스에 연결하지 못했습니다. Qdrant가 실행 중이고 {{qdrantUrl}}에서 접근 가능한지 확인하세요. 오류: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "인증에 실패했습니다. 설정에서 API 키를 확인하세요.",
"connectionFailed": "임베더 서비스에 연결하지 못했습니다. 연결 설정을 확인하고 서비스가 실행 중인지 확인하세요.",
"modelNotAvailable": "지정된 모델을 사용할 수 없습니다. 모델 구성을 확인하세요.",
"configurationError": "잘못된 임베더 구성입니다. 설정을 검토하세요.",
"serviceUnavailable": "임베더 서비스를 사용할 수 없습니다. 실행 중이고 액세스 가능한지 확인하세요.",
"invalidEndpoint": "잘못된 API 엔드포인트입니다. URL 구성을 확인하세요.",
"invalidEmbedderConfig": "잘못된 임베더 구성입니다. 설정을 확인하세요.",
"invalidApiKey": "잘못된 API 키입니다. API 키 구성을 확인하세요.",
"invalidBaseUrl": "잘못된 기본 URL입니다. URL 구성을 확인하세요.",
"invalidModel": "잘못된 모델입니다. 모델 구성을 확인하세요.",
"invalidResponse": "임베더 서비스에서 잘못된 응답이 왔습니다. 구성을 확인하세요."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?",
"delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}",
"delete_message": "Wat wil je verwijderen?",
"just_this_message": "Alleen dit bericht",
"this_and_subsequent": "Dit en alle volgende berichten"
"delete_just_this_message": "Alleen dit bericht",
"delete_this_and_subsequent": "Dit en alle volgende berichten",
"edit_message": "Alle berichten na dit bericht verwijderen?",
"edit_just_this_message": "Nee, alleen dit bericht bewerken",
"edit_this_and_delete_subsequent": "Ja"
},
"errors": {
"invalid_data_uri": "Ongeldig data-URI-formaat",
@ -108,6 +111,11 @@
"remove": "Verwijderen",
"keep": "Behouden"
},
"buttons": {
"save": "Opslaan",
"cancel": "Annuleren",
"edit": "Bewerken"
},
"tasks": {
"canceled": "Taakfout: gestopt en geannuleerd door gebruiker.",
"deleted": "Taakfout: gestopt en verwijderd door gebruiker.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Kon foutinhoud niet lezen",
"requestFailed": "Ollama API-verzoek mislukt met status {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Ongeldige responsstructuur van Ollama API: \"embeddings\" array niet gevonden of is geen array.",
"embeddingFailed": "Ollama insluiting mislukt: {{message}}"
"embeddingFailed": "Ollama insluiting mislukt: {{message}}",
"serviceNotRunning": "Ollama-service draait niet op {{baseUrl}}",
"serviceUnavailable": "Ollama-service is niet beschikbaar (status: {{status}})",
"modelNotFound": "Ollama-model niet gevonden: {{modelId}}",
"modelNotEmbeddingCapable": "Ollama-model is niet in staat tot insluiten: {{modelId}}",
"hostNotFound": "Ollama-host niet gevonden: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Onbekende fout bij verwerken van bestand {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Kan geen verbinding maken met Qdrant vectordatabase. Zorg ervoor dat Qdrant draait en toegankelijk is op {{qdrantUrl}}. Fout: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Authenticatie mislukt. Controleer je API-sleutel in de instellingen.",
"connectionFailed": "Verbinding met de embedder-service mislukt. Controleer je verbindingsinstellingen en zorg ervoor dat de service draait.",
"modelNotAvailable": "Het opgegeven model is niet beschikbaar. Controleer je modelconfiguratie.",
"configurationError": "Ongeldige embedder-configuratie. Controleer je instellingen.",
"serviceUnavailable": "De embedder-service is niet beschikbaar. Zorg ervoor dat deze draait en toegankelijk is.",
"invalidEndpoint": "Ongeldig API-eindpunt. Controleer je URL-configuratie.",
"invalidEmbedderConfig": "Ongeldige embedder-configuratie. Controleer je instellingen.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?",
"delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}",
"delete_message": "Co chcesz usunąć?",
"just_this_message": "Tylko tę wiadomość",
"this_and_subsequent": "Tę i wszystkie kolejne wiadomości"
"delete_just_this_message": "Tylko tę wiadomość",
"delete_this_and_subsequent": "Tę i wszystkie kolejne wiadomości",
"edit_message": "Usunąć wszystkie wiadomości po tej?",
"edit_just_this_message": "Nie, tylko edytuj tę wiadomość",
"edit_this_and_delete_subsequent": "Tak"
},
"errors": {
"invalid_data_uri": "Nieprawidłowy format URI danych",
@ -108,6 +111,11 @@
"remove": "Usuń",
"keep": "Zachowaj"
},
"buttons": {
"save": "Zapisz",
"cancel": "Anuluj",
"edit": "Edytuj"
},
"tasks": {
"canceled": "Błąd zadania: Zostało zatrzymane i anulowane przez użytkownika.",
"deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Nie można odczytać treści błędu",
"requestFailed": "Żądanie API Ollama nie powiodło się ze statusem {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Nieprawidłowa struktura odpowiedzi z API Ollama: tablica \"embeddings\" nie została znaleziona lub nie jest tablicą.",
"embeddingFailed": "Osadzenie Ollama nie powiodło się: {{message}}"
"embeddingFailed": "Osadzenie Ollama nie powiodło się: {{message}}",
"serviceNotRunning": "Usługa Ollama nie jest uruchomiona pod adresem {{baseUrl}}",
"serviceUnavailable": "Usługa Ollama jest niedostępna (status: {{status}})",
"modelNotFound": "Nie znaleziono modelu Ollama: {{modelId}}",
"modelNotEmbeddingCapable": "Model Ollama nie jest zdolny do osadzania: {{modelId}}",
"hostNotFound": "Nie znaleziono hosta Ollama: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Nieznany błąd podczas przetwarzania pliku {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Uwierzytelnianie nie powiodło się. Sprawdź swój klucz API w ustawieniach.",
"connectionFailed": "Nie udało się połączyć z usługą embeddera. Sprawdź ustawienia połączenia i upewnij się, że usługa jest uruchomiona.",
"modelNotAvailable": "Określony model jest niedostępny. Sprawdź konfigurację modelu.",
"configurationError": "Nieprawidłowa konfiguracja embeddera. Sprawdź swoje ustawienia.",
"serviceUnavailable": "Usługa embeddera jest niedostępna. Upewnij się, że jest uruchomiona i dostępna.",
"invalidEndpoint": "Nieprawidłowy punkt końcowy API. Sprawdź konfigurację adresu URL.",
"invalidEmbedderConfig": "Nieprawidłowa konfiguracja embeddera. Sprawdź swoje ustawienia.",
"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ę."
}
}

View file

@ -23,8 +23,11 @@
"delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?",
"delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}",
"delete_message": "O que você gostaria de excluir?",
"just_this_message": "Apenas esta mensagem",
"this_and_subsequent": "Esta e todas as mensagens subsequentes"
"delete_just_this_message": "Apenas esta mensagem",
"delete_this_and_subsequent": "Esta e todas as mensagens subsequentes",
"edit_message": "Excluir todas as mensagens após esta?",
"edit_just_this_message": "Não, apenas editar esta",
"edit_this_and_delete_subsequent": "Sim"
},
"errors": {
"invalid_data_uri": "Formato de URI de dados inválido",
@ -112,6 +115,11 @@
"remove": "Remover",
"keep": "Manter"
},
"buttons": {
"save": "Salvar",
"cancel": "Cancelar",
"edit": "Editar"
},
"tasks": {
"canceled": "Erro na tarefa: Foi interrompida e cancelada pelo usuário.",
"deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Não foi possível ler o corpo do erro",
"requestFailed": "Solicitação da API Ollama falhou com status {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Estrutura de resposta inválida da API Ollama: array \"embeddings\" não encontrado ou não é um array.",
"embeddingFailed": "Embedding Ollama falhou: {{message}}"
"embeddingFailed": "Embedding Ollama falhou: {{message}}",
"serviceNotRunning": "O serviço Ollama não está em execução em {{baseUrl}}",
"serviceUnavailable": "O serviço Ollama não está disponível (status: {{status}})",
"modelNotFound": "Modelo Ollama não encontrado: {{modelId}}",
"modelNotEmbeddingCapable": "O modelo Ollama não é capaz de embedding: {{modelId}}",
"hostNotFound": "Host Ollama não encontrado: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Erro desconhecido ao processar arquivo {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Falha na autenticação. Verifique sua chave de API nas configurações.",
"connectionFailed": "Falha ao conectar ao serviço do embedder. Verifique suas configurações de conexão e garanta que o serviço esteja em execução.",
"modelNotAvailable": "O modelo especificado não está disponível. Verifique a configuração do seu modelo.",
"configurationError": "Configuração do embedder inválida. Revise suas configurações.",
"serviceUnavailable": "O serviço do embedder não está disponível. Garanta que ele esteja em execução e acessível.",
"invalidEndpoint": "Endpoint de API inválido. Verifique sua configuração de URL.",
"invalidEmbedderConfig": "Configuração do embedder inválida. Verifique suas configurações.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?",
"delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}",
"delete_message": "Что вы хотите удалить?",
"just_this_message": "Только это сообщение",
"this_and_subsequent": "Это и все последующие сообщения"
"delete_just_this_message": "Только это сообщение",
"delete_this_and_subsequent": "Это и все последующие сообщения",
"edit_message": "Удалить все сообщения после этого?",
"edit_just_this_message": "Нет, только редактировать это",
"edit_this_and_delete_subsequent": "Да"
},
"errors": {
"invalid_data_uri": "Неверный формат URI данных",
@ -108,6 +111,11 @@
"remove": "Удалить",
"keep": "Оставить"
},
"buttons": {
"save": "Сохранить",
"cancel": "Отмена",
"edit": "Редактировать"
},
"tasks": {
"canceled": "Ошибка задачи: Она была остановлена и отменена пользователем.",
"deleted": "Сбой задачи: Она была остановлена и удалена пользователем.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Не удалось прочитать тело ошибки",
"requestFailed": "Запрос к API Ollama не удался со статусом {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Неверная структура ответа от API Ollama: массив \"embeddings\" не найден или не является массивом.",
"embeddingFailed": "Вложение Ollama не удалось: {{message}}"
"embeddingFailed": "Вложение Ollama не удалось: {{message}}",
"serviceNotRunning": "Сервис Ollama не запущен по адресу {{baseUrl}}",
"serviceUnavailable": "Сервис Ollama недоступен (статус: {{status}})",
"modelNotFound": "Модель Ollama не найдена: {{modelId}}",
"modelNotEmbeddingCapable": "Модель Ollama не способна к вложению: {{modelId}}",
"hostNotFound": "Хост Ollama не найден: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Неизвестная ошибка при обработке файла {{filePath}}",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "Не удалось подключиться к векторной базе данных Qdrant. Убедитесь, что Qdrant запущен и доступен по адресу {{qdrantUrl}}. Ошибка: {{errorMessage}}"
},
"validation": {
"authenticationFailed": "Ошибка аутентификации. Проверьте свой ключ API в настройках.",
"connectionFailed": "Не удалось подключиться к службе эмбеддера. Проверьте настройки подключения и убедитесь, что служба запущена.",
"modelNotAvailable": "Указанная модель недоступна. Проверьте конфигурацию модели.",
"configurationError": "Неверная конфигурация эмбеддера. Проверьте свои настройки.",
"serviceUnavailable": "Служба эмбеддера недоступна. Убедитесь, что она запущена и доступна.",
"invalidEndpoint": "Неверная конечная точка API. Проверьте конфигурацию URL.",
"invalidEmbedderConfig": "Неверная конфигурация эмбеддера. Проверьте свои настройки.",
"invalidApiKey": "Неверный ключ API. Проверьте конфигурацию ключа API.",
"invalidBaseUrl": "Неверный базовый URL. Проверьте конфигурацию URL.",
"invalidModel": "Неверная модель. Проверьте конфигурацию модели.",
"invalidResponse": "Неверный ответ от службы embedder. Проверьте вашу конфигурацию."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?",
"delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}",
"delete_message": "Neyi silmek istersiniz?",
"just_this_message": "Sadece bu mesajı",
"this_and_subsequent": "Bu ve sonraki tüm mesajları"
"delete_just_this_message": "Sadece bu mesajı",
"delete_this_and_subsequent": "Bu ve sonraki tüm mesajları",
"edit_message": "Bu mesajdan sonraki tüm mesajlar silinsin mi?",
"edit_just_this_message": "Hayır, sadece bunu düzenle",
"edit_this_and_delete_subsequent": "Evet"
},
"errors": {
"invalid_data_uri": "Geçersiz veri URI formatı",
@ -108,6 +111,11 @@
"remove": "Kaldır",
"keep": "Koru"
},
"buttons": {
"save": "Kaydet",
"cancel": "İptal",
"edit": "Düzenle"
},
"tasks": {
"canceled": "Görev hatası: Kullanıcı tarafından durduruldu ve iptal edildi.",
"deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Hata gövdesi okunamadı",
"requestFailed": "Ollama API isteği {{status}} {{statusText}} durumuyla başarısız oldu: {{errorBody}}",
"invalidResponseStructure": "Ollama API'den geçersiz yanıt yapısı: \"embeddings\" dizisi bulunamadı veya dizi değil.",
"embeddingFailed": "Ollama gömülmesi başarısız oldu: {{message}}"
"embeddingFailed": "Ollama gömülmesi başarısız oldu: {{message}}",
"serviceNotRunning": "Ollama hizmeti {{baseUrl}} adresinde çalışmıyor",
"serviceUnavailable": "Ollama hizmeti kullanılamıyor (durum: {{status}})",
"modelNotFound": "Ollama modeli bulunamadı: {{modelId}}",
"modelNotEmbeddingCapable": "Ollama modeli gömme yeteneğine sahip değil: {{modelId}}",
"hostNotFound": "Ollama ana bilgisayarı bulunamadı: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "{{filePath}} dosyası işlenirken bilinmeyen hata",
@ -19,5 +24,18 @@
},
"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}}"
},
"validation": {
"authenticationFailed": "Kimlik doğrulama başarısız oldu. Lütfen ayarlardan API anahtarınızı kontrol edin.",
"connectionFailed": "Gömücü hizmetine bağlanılamadı. Lütfen bağlantı ayarlarınızı kontrol edin ve hizmetin çalıştığından emin olun.",
"modelNotAvailable": "Belirtilen model mevcut değil. Lütfen model yapılandırmanızı kontrol edin.",
"configurationError": "Geçersiz gömücü yapılandırması. Lütfen ayarlarınızı gözden geçirin.",
"serviceUnavailable": "Gömücü hizmeti mevcut değil. Lütfen çalıştığından ve erişilebilir olduğundan emin olun.",
"invalidEndpoint": "Geçersiz API uç noktası. Lütfen URL yapılandırmanızı kontrol edin.",
"invalidEmbedderConfig": "Geçersiz gömücü yapılandırması. Lütfen ayarlarınızı kontrol edin.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "Bạn có chắc chắn muốn xóa hồ sơ cấu hình này không?",
"delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}",
"delete_message": "Bạn muốn xóa gì?",
"just_this_message": "Chỉ tin nhắn này",
"this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo"
"delete_just_this_message": "Chỉ tin nhắn này",
"delete_this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo",
"edit_message": "Xóa tất cả tin nhắn sau tin nhắn này?",
"edit_just_this_message": "Không, chỉ chỉnh sửa tin nhắn này",
"edit_this_and_delete_subsequent": "Có"
},
"errors": {
"invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ",
@ -108,6 +111,11 @@
"remove": "Xóa",
"keep": "Giữ"
},
"buttons": {
"save": "Lưu",
"cancel": "Hủy",
"edit": "Chỉnh sửa"
},
"tasks": {
"canceled": "Lỗi nhiệm vụ: Nó đã bị dừng và hủy bởi người dùng.",
"deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng.",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "Không thể đọc nội dung lỗi",
"requestFailed": "Yêu cầu API Ollama thất bại với trạng thái {{status}} {{statusText}}: {{errorBody}}",
"invalidResponseStructure": "Cấu trúc phản hồi không hợp lệ từ API Ollama: không tìm thấy mảng \"embeddings\" hoặc không phải là mảng.",
"embeddingFailed": "Nhúng Ollama thất bại: {{message}}"
"embeddingFailed": "Nhúng Ollama thất bại: {{message}}",
"serviceNotRunning": "Dịch vụ Ollama không chạy tại {{baseUrl}}",
"serviceUnavailable": "Dịch vụ Ollama không khả dụng (trạng thái: {{status}})",
"modelNotFound": "Không tìm thấy mô hình Ollama: {{modelId}}",
"modelNotEmbeddingCapable": "Mô hình Ollama không có khả năng nhúng: {{modelId}}",
"hostNotFound": "Không tìm thấy máy chủ Ollama: {{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "Lỗi không xác định khi xử lý tệp {{filePath}}",
@ -19,5 +24,18 @@
},
"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}}"
},
"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.",
"connectionFailed": "Không thể kết nối với dịch vụ nhúng. Vui lòng kiểm tra cài đặt kết nối của bạn và đảm bảo dịch vụ đang chạy.",
"modelNotAvailable": "Mô hình được chỉ định không có sẵn. Vui lòng kiểm tra cấu hình mô hình của bạn.",
"configurationError": "Cấu hình nhúng không hợp lệ. Vui lòng xem lại cài đặt của bạn.",
"serviceUnavailable": "Dịch vụ nhúng không có sẵn. Vui lòng đảm bảo nó đang chạy và có thể truy cập được.",
"invalidEndpoint": "Điểm cuối API không hợp lệ. Vui lòng kiểm tra cấu hình URL của bạn.",
"invalidEmbedderConfig": "Cấu hình nhúng không hợp lệ. Vui lòng kiểm tra cài đặt của bạn.",
"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."
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "您确定要删除此配置文件吗?",
"delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹\n{rulesFolderPath}",
"delete_message": "您想删除什么?",
"just_this_message": "仅此消息",
"this_and_subsequent": "此消息及所有后续消息"
"edit_message": "删除此消息后的所有消息?",
"delete_just_this_message": "仅此消息",
"edit_just_this_message": "不,仅编辑此消息",
"delete_this_and_subsequent": "此消息及所有后续消息",
"edit_this_and_delete_subsequent": "是"
},
"errors": {
"invalid_mcp_config": "项目MCP配置格式无效",
@ -113,6 +116,11 @@
"remove": "删除",
"keep": "保留"
},
"buttons": {
"save": "保存",
"cancel": "取消",
"edit": "编辑"
},
"tasks": {
"canceled": "任务错误:它已被用户停止并取消。",
"deleted": "任务失败:它已被用户停止并删除。",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "无法读取错误内容",
"requestFailed": "Ollama API 请求失败,状态码 {{status}} {{statusText}}{{errorBody}}",
"invalidResponseStructure": "Ollama API 响应结构无效:未找到 \"embeddings\" 数组或不是数组。",
"embeddingFailed": "Ollama 嵌入失败:{{message}}"
"embeddingFailed": "Ollama 嵌入失败:{{message}}",
"serviceNotRunning": "Ollama 服务未在 {{baseUrl}} 运行",
"serviceUnavailable": "Ollama 服务不可用(状态:{{status}}",
"modelNotFound": "未找到 Ollama 模型:{{modelId}}",
"modelNotEmbeddingCapable": "Ollama 模型不具备嵌入能力:{{modelId}}",
"hostNotFound": "未找到 Ollama 主机:{{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "处理文件 {{filePath}} 时出现未知错误",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "连接 Qdrant 向量数据库失败。请确保 Qdrant 正在运行并可在 {{qdrantUrl}} 访问。错误:{{errorMessage}}"
},
"validation": {
"authenticationFailed": "身份验证失败。请在设置中检查您的 API 密钥。",
"connectionFailed": "连接嵌入器服务失败。请检查您的连接设置并确保服务正在运行。",
"modelNotAvailable": "指定的模型不可用。请检查您的模型配置。",
"configurationError": "嵌入器配置无效。请查看您的设置。",
"serviceUnavailable": "嵌入器服务不可用。请确保它正在运行且可访问。",
"invalidEndpoint": "API 端点无效。请检查您的 URL 配置。",
"invalidEmbedderConfig": "嵌入器配置无效。请检查您的设置。",
"invalidApiKey": "API 密钥无效。请检查您的 API 密钥配置。",
"invalidBaseUrl": "基础 URL 无效。请检查您的 URL 配置。",
"invalidModel": "模型无效。请检查您的模型配置。",
"invalidResponse": "嵌入服务响应无效。请检查您的配置。"
}
}

View file

@ -19,8 +19,11 @@
"delete_config_profile": "您確定要刪除此設定檔案嗎?",
"delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾\n{rulesFolderPath}",
"delete_message": "您想刪除哪些內容?",
"just_this_message": "僅這則訊息",
"this_and_subsequent": "這則訊息及所有後續訊息"
"edit_message": "刪除此訊息後的所有訊息?",
"delete_just_this_message": "僅這則訊息",
"edit_just_this_message": "否,僅編輯此訊息",
"delete_this_and_subsequent": "這則訊息及所有後續訊息",
"edit_this_and_delete_subsequent": "是"
},
"errors": {
"invalid_data_uri": "資料 URI 格式無效",
@ -108,6 +111,11 @@
"remove": "刪除",
"keep": "保留"
},
"buttons": {
"save": "儲存",
"cancel": "取消",
"edit": "編輯"
},
"tasks": {
"canceled": "工作錯誤:它已被使用者停止並取消。",
"deleted": "工作失敗:它已被使用者停止並刪除。",

View file

@ -10,7 +10,12 @@
"couldNotReadErrorBody": "無法讀取錯誤內容",
"requestFailed": "Ollama API 請求失敗,狀態碼 {{status}} {{statusText}}{{errorBody}}",
"invalidResponseStructure": "Ollama API 回應結構無效:未找到 \"embeddings\" 陣列或不是陣列。",
"embeddingFailed": "Ollama 內嵌失敗:{{message}}"
"embeddingFailed": "Ollama 內嵌失敗:{{message}}",
"serviceNotRunning": "Ollama 服務未在 {{baseUrl}} 執行",
"serviceUnavailable": "Ollama 服務不可用(狀態:{{status}}",
"modelNotFound": "找不到 Ollama 模型:{{modelId}}",
"modelNotEmbeddingCapable": "Ollama 模型不具備內嵌能力:{{modelId}}",
"hostNotFound": "找不到 Ollama 主機:{{baseUrl}}"
},
"scanner": {
"unknownErrorProcessingFile": "處理檔案 {{filePath}} 時發生未知錯誤",
@ -19,5 +24,18 @@
},
"vectorStore": {
"qdrantConnectionFailed": "連接 Qdrant 向量資料庫失敗。請確保 Qdrant 正在執行並可在 {{qdrantUrl}} 存取。錯誤:{{errorMessage}}"
},
"validation": {
"authenticationFailed": "驗證失敗。請在設定中檢查您的 API 金鑰。",
"connectionFailed": "連線至內嵌服務失敗。請檢查您的連線設定並確保服務正在執行。",
"modelNotAvailable": "指定的模型不可用。請檢查您的模型組態。",
"configurationError": "無效的內嵌程式組態。請檢閱您的設定。",
"serviceUnavailable": "內嵌服務不可用。請確保它正在執行且可存取。",
"invalidEndpoint": "無效的 API 端點。請檢查您的 URL 組態。",
"invalidEmbedderConfig": "無效的內嵌程式組態。請檢查您的設定。",
"invalidApiKey": "無效的 API 金鑰。請檢查您的 API 金鑰組態。",
"invalidBaseUrl": "無效的基礎 URL。請檢查您的 URL 組態。",
"invalidModel": "無效的模型。請檢查您的模型組態。",
"invalidResponse": "內嵌服務回應無效。請檢查您的組態。"
}
}

View file

@ -353,7 +353,7 @@
"vsix": "mkdirp ../bin && vsce package --no-dependencies --out ../bin",
"publish:marketplace": "vsce publish --no-dependencies && ovsx publish --no-dependencies",
"watch:bundle": "pnpm bundle --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch:tsc": "cd .. && tsc --noEmit --watch --project src/tsconfig.json",
"clean": "rimraf README.md CHANGELOG.md LICENSE dist mock .turbo"
},
"dependencies": {

View file

@ -1,3 +1,7 @@
import { CodeIndexManager } from "../manager"
import { CodeIndexServiceFactory } from "../service-factory"
import type { MockedClass } from "vitest"
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
@ -21,10 +25,12 @@ vi.mock("../state-manager", () => ({
onProgressUpdate: vi.fn(),
getCurrentStatus: vi.fn(),
dispose: vi.fn(),
setSystemState: vi.fn(),
})),
}))
import { CodeIndexManager } from "../manager"
vi.mock("../service-factory")
const MockedCodeIndexServiceFactory = CodeIndexServiceFactory as MockedClass<typeof CodeIndexServiceFactory>
describe("CodeIndexManager - handleSettingsChange regression", () => {
let mockContext: any
@ -72,13 +78,63 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
// Mock a minimal config manager that simulates first-time configuration
const mockConfigManager = {
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }),
isFeatureConfigured: true,
isFeatureEnabled: true,
getConfig: vi.fn().mockReturnValue({
isEnabled: true,
isConfigured: true,
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: { openAiNativeApiKey: "test-key" },
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
searchMinScore: 0.4,
}),
}
;(manager as any)._configManager = mockConfigManager
// Mock cache manager
const mockCacheManager = {
initialize: vi.fn(),
clearCacheFile: vi.fn(),
}
;(manager as any)._cacheManager = mockCacheManager
// Mock the feature state to simulate valid configuration that would normally trigger restart
vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true)
vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true)
// Mock service factory to handle _recreateServices call
const mockServiceFactoryInstance = {
configManager: mockConfigManager,
workspacePath: "/test/workspace",
cacheManager: mockCacheManager,
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
createVectorStore: vi.fn().mockReturnValue({}),
createDirectoryScanner: vi.fn().mockReturnValue({}),
createFileWatcher: vi.fn().mockReturnValue({
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}),
createServices: vi.fn().mockReturnValue({
embedder: { embedderInfo: { name: "openai" } },
vectorStore: {},
scanner: {},
fileWatcher: {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
},
}),
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
}
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
// The key test: this should NOT throw "CodeIndexManager not initialized" error
await expect(manager.handleSettingsChange()).resolves.not.toThrow()
@ -105,29 +161,65 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
}
;(manager as any)._configManager = mockConfigManager
// Mock cache manager
const mockCacheManager = {
initialize: vi.fn(),
clearCacheFile: vi.fn(),
}
;(manager as any)._cacheManager = mockCacheManager
// Simulate an initialized manager by setting the required properties
;(manager as any)._orchestrator = { stopWatcher: vi.fn() }
;(manager as any)._searchService = {}
;(manager as any)._cacheManager = {}
// Verify manager is considered initialized
expect(manager.isInitialized).toBe(true)
// Mock the methods that would be called during restart
const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices").mockImplementation(() => {})
const startIndexingSpy = vi.spyOn(manager, "startIndexing").mockResolvedValue()
// Mock the feature state
vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true)
vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true)
// Mock service factory to handle _recreateServices call
const mockServiceFactoryInstance = {
configManager: mockConfigManager,
workspacePath: "/test/workspace",
cacheManager: mockCacheManager,
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
createVectorStore: vi.fn().mockReturnValue({}),
createDirectoryScanner: vi.fn().mockReturnValue({}),
createFileWatcher: vi.fn().mockReturnValue({
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}),
createServices: vi.fn().mockReturnValue({
embedder: { embedderInfo: { name: "openai" } },
vectorStore: {},
scanner: {},
fileWatcher: {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
},
}),
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
}
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
// Mock the methods that would be called during restart
const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices")
await manager.handleSettingsChange()
// Verify that the restart sequence was called
expect(mockConfigManager.loadConfiguration).toHaveBeenCalled()
// stopWatcher is called inside _recreateServices, which we mocked
// _recreateServices should be called when requiresRestart is true
expect(recreateServicesSpy).toHaveBeenCalled()
expect(startIndexingSpy).toHaveBeenCalled()
// Note: startIndexing is NOT called by handleSettingsChange - it's only called by initialize()
})
it("should handle case when config manager is not set", async () => {
@ -138,4 +230,135 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
await expect(manager.handleSettingsChange()).resolves.not.toThrow()
})
})
describe("embedder validation integration", () => {
let mockServiceFactoryInstance: any
let mockStateManager: any
let mockEmbedder: any
let mockVectorStore: any
let mockScanner: any
let mockFileWatcher: any
beforeEach(() => {
// Mock service factory objects
mockEmbedder = { embedderInfo: { name: "openai" } }
mockVectorStore = {}
mockScanner = {}
mockFileWatcher = {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}
// Mock service factory instance
mockServiceFactoryInstance = {
createServices: vi.fn().mockReturnValue({
embedder: mockEmbedder,
vectorStore: mockVectorStore,
scanner: mockScanner,
fileWatcher: mockFileWatcher,
}),
validateEmbedder: vi.fn(),
}
// Mock the ServiceFactory constructor
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance)
// Mock state manager methods directly on the existing instance
mockStateManager = (manager as any)._stateManager
mockStateManager.setSystemState = vi.fn()
// Mock config manager
const mockConfigManager = {
loadConfiguration: vitest.fn().mockResolvedValue({ requiresRestart: false }),
isFeatureConfigured: true,
isFeatureEnabled: true,
getConfig: vitest.fn().mockReturnValue({
isEnabled: true,
isConfigured: true,
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: { openAiNativeApiKey: "test-key" },
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
searchMinScore: 0.4,
}),
}
;(manager as any)._configManager = mockConfigManager
})
it("should validate embedder during _recreateServices when validation succeeds", async () => {
// Arrange
mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ valid: true })
// Act - directly call the private method for testing
await (manager as any)._recreateServices()
// Assert
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String))
})
it("should set error state when embedder validation fails", async () => {
// Arrange
mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({
valid: false,
error: "embeddings:validation.authenticationFailed",
})
// Act & Assert
await expect((manager as any)._recreateServices()).rejects.toThrow(
"embeddings:validation.authenticationFailed",
)
// Assert other expectations
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
"Error",
"embeddings:validation.authenticationFailed",
)
})
it("should set generic error state when embedder validation throws", async () => {
// Arrange
// Since the real service factory catches exceptions, we should mock it to resolve with an error
mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({
valid: false,
error: "embeddings:validation.configurationError",
})
// Act & Assert
await expect((manager as any)._recreateServices()).rejects.toThrow(
"embeddings:validation.configurationError",
)
// Assert other expectations
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
"Error",
"embeddings:validation.configurationError",
)
})
it("should handle embedder creation failure", async () => {
// Arrange
mockServiceFactoryInstance.createServices.mockImplementation(() => {
throw new Error("Invalid configuration")
})
// Act & Assert - should throw the error
await expect((manager as any)._recreateServices()).rejects.toThrow("Invalid configuration")
// Should not attempt validation if embedder creation fails
expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled()
})
})
})

View file

@ -580,4 +580,187 @@ describe("CodeIndexServiceFactory", () => {
expect(() => factory.createVectorStore()).toThrow("Qdrant URL missing for vector store creation")
})
})
describe("validateEmbedder", () => {
let mockEmbedderInstance: any
beforeEach(() => {
mockEmbedderInstance = {
validateConfiguration: vitest.fn(),
}
})
it("should validate OpenAI embedder successfully", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: {
openAiNativeApiKey: "test-api-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance)
mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true })
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({ valid: true })
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})
it("should return validation error from OpenAI embedder", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: {
openAiNativeApiKey: "invalid-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance)
mockEmbedderInstance.validateConfiguration.mockResolvedValue({
valid: false,
error: "embeddings:validation.authenticationFailed",
})
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({
valid: false,
error: "embeddings:validation.authenticationFailed",
})
})
it("should validate Ollama embedder successfully", async () => {
// Arrange
const testConfig = {
embedderProvider: "ollama",
modelId: "nomic-embed-text",
ollamaOptions: {
ollamaBaseUrl: "http://localhost:11434",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedCodeIndexOllamaEmbedder.mockImplementation(() => mockEmbedderInstance)
mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true })
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({ valid: true })
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})
it("should validate OpenAI Compatible embedder successfully", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai-compatible",
modelId: "custom-model",
openAiCompatibleOptions: {
baseUrl: "https://api.example.com/v1",
apiKey: "test-api-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedOpenAICompatibleEmbedder.mockImplementation(() => mockEmbedderInstance)
mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true })
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({ valid: true })
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})
it("should validate Gemini embedder successfully", async () => {
// Arrange
const testConfig = {
embedderProvider: "gemini",
geminiOptions: {
apiKey: "test-gemini-api-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedGeminiEmbedder.mockImplementation(() => mockEmbedderInstance)
mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true })
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({ valid: true })
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})
it("should handle validation exceptions", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: {
openAiNativeApiKey: "test-api-key",
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance)
const networkError = new Error("Network error")
mockEmbedderInstance.validateConfiguration.mockRejectedValue(networkError)
// Act
const embedder = factory.createEmbedder()
const result = await factory.validateEmbedder(embedder)
// Assert
expect(result).toEqual({
valid: false,
error: "Network error",
})
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
})
it("should return error for invalid embedder configuration", async () => {
// Arrange
const testConfig = {
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: {
openAiNativeApiKey: undefined, // Missing API key
},
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
// Act & Assert
// This should throw when trying to create the embedder
await expect(async () => {
const embedder = factory.createEmbedder()
await factory.validateEmbedder(embedder)
}).rejects.toThrow("OpenAI configuration missing for embedder creation")
})
it("should return error for unknown embedder provider", async () => {
// Arrange
const testConfig = {
embedderProvider: "unknown-provider",
modelId: "some-model",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
// Act & Assert
// This should throw when trying to create the embedder
expect(() => factory.createEmbedder()).toThrow("Invalid embedder type configured: unknown-provider")
})
})
})

View file

@ -55,4 +55,54 @@ describe("GeminiEmbedder", () => {
expect(GeminiEmbedder.dimension).toBe(768)
})
})
describe("validateConfiguration", () => {
let mockValidateConfiguration: any
beforeEach(() => {
mockValidateConfiguration = vitest.fn()
MockedOpenAICompatibleEmbedder.prototype.validateConfiguration = mockValidateConfiguration
})
it("should delegate validation to OpenAICompatibleEmbedder", async () => {
// Arrange
embedder = new GeminiEmbedder("test-api-key")
mockValidateConfiguration.mockResolvedValue({ valid: true })
// Act
const result = await embedder.validateConfiguration()
// Assert
expect(mockValidateConfiguration).toHaveBeenCalled()
expect(result).toEqual({ valid: true })
})
it("should pass through validation errors from OpenAICompatibleEmbedder", async () => {
// Arrange
embedder = new GeminiEmbedder("test-api-key")
mockValidateConfiguration.mockResolvedValue({
valid: false,
error: "embeddings:validation.authenticationFailed",
})
// Act
const result = await embedder.validateConfiguration()
// Assert
expect(mockValidateConfiguration).toHaveBeenCalled()
expect(result).toEqual({
valid: false,
error: "embeddings:validation.authenticationFailed",
})
})
it("should handle validation exceptions", async () => {
// Arrange
embedder = new GeminiEmbedder("test-api-key")
mockValidateConfiguration.mockRejectedValue(new Error("Validation failed"))
// Act & Assert
await expect(embedder.validateConfiguration()).rejects.toThrow("Validation failed")
})
})
})

View file

@ -0,0 +1,238 @@
import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest"
import type { MockedFunction } from "vitest"
import { CodeIndexOllamaEmbedder } from "../ollama"
// Mock fetch
global.fetch = vitest.fn() as MockedFunction<typeof fetch>
// Mock i18n
vitest.mock("../../../../i18n", () => ({
t: (key: string, params?: Record<string, any>) => {
const translations: Record<string, string> = {
"embeddings:validation.serviceUnavailable":
"The embedder service is not available. Please ensure it is running and accessible.",
"embeddings:validation.modelNotAvailable":
"The specified model is not available. Please check your model configuration.",
"embeddings:validation.connectionFailed":
"Failed to connect to the embedder service. Please check your connection settings and ensure the service is running.",
"embeddings:validation.configurationError": "Invalid embedder configuration. Please review your settings.",
"embeddings:errors.ollama.serviceNotRunning":
"Ollama service is not running at {{baseUrl}}. Please start Ollama first.",
"embeddings:errors.ollama.serviceUnavailable":
"Ollama service is unavailable at {{baseUrl}}. HTTP status: {{status}}",
"embeddings:errors.ollama.modelNotFound":
"Model '{{model}}' not found. Available models: {{availableModels}}",
"embeddings:errors.ollama.modelNotEmbedding": "Model '{{model}}' is not embedding capable",
"embeddings:errors.ollama.hostNotFound": "Ollama host not found: {{baseUrl}}",
"embeddings:errors.ollama.connectionTimeout": "Connection to Ollama timed out at {{baseUrl}}",
}
// Handle parameter substitution
let result = translations[key] || key
if (params) {
Object.entries(params).forEach(([param, value]) => {
result = result.replace(new RegExp(`{{${param}}}`, "g"), String(value))
})
}
return result
},
}))
// Mock console methods
const consoleMocks = {
error: vitest.spyOn(console, "error").mockImplementation(() => {}),
}
describe("CodeIndexOllamaEmbedder", () => {
let embedder: CodeIndexOllamaEmbedder
let mockFetch: MockedFunction<typeof fetch>
beforeEach(() => {
vitest.clearAllMocks()
consoleMocks.error.mockClear()
mockFetch = global.fetch as MockedFunction<typeof fetch>
embedder = new CodeIndexOllamaEmbedder({
ollamaModelId: "nomic-embed-text",
ollamaBaseUrl: "http://localhost:11434",
})
})
afterEach(() => {
vitest.clearAllMocks()
})
describe("constructor", () => {
it("should initialize with provided options", () => {
expect(embedder.embedderInfo.name).toBe("ollama")
})
it("should use default values when not provided", () => {
const embedderWithDefaults = new CodeIndexOllamaEmbedder({})
expect(embedderWithDefaults.embedderInfo.name).toBe("ollama")
})
})
describe("validateConfiguration", () => {
it("should validate successfully when service is available and model exists", async () => {
// Mock successful /api/tags call
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
models: [{ name: "nomic-embed-text:latest" }, { name: "llama2:latest" }],
}),
} as Response),
)
// Mock successful /api/embed test call
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
embeddings: [[0.1, 0.2, 0.3]],
}),
} as Response),
)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(mockFetch).toHaveBeenCalledTimes(2)
// Check first call (GET /api/tags)
const firstCall = mockFetch.mock.calls[0]
expect(firstCall[0]).toBe("http://localhost:11434/api/tags")
expect(firstCall[1]?.method).toBe("GET")
expect(firstCall[1]?.headers).toEqual({ "Content-Type": "application/json" })
expect(firstCall[1]?.signal).toBeDefined() // AbortSignal for timeout
// Check second call (POST /api/embed)
const secondCall = mockFetch.mock.calls[1]
expect(secondCall[0]).toBe("http://localhost:11434/api/embed")
expect(secondCall[1]?.method).toBe("POST")
expect(secondCall[1]?.headers).toEqual({ "Content-Type": "application/json" })
expect(secondCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", input: ["test"] }))
expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout
})
it("should fail validation when service is not available", async () => {
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Connection to Ollama timed out at http://localhost:11434")
})
it("should fail validation when tags endpoint returns 404", async () => {
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: false,
status: 404,
} as Response),
)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe(
"Ollama service is not running at http://localhost:11434. Please start Ollama first.",
)
})
it("should fail validation when tags endpoint returns other error", async () => {
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: false,
status: 500,
} as Response),
)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Ollama service is unavailable at http://localhost:11434. HTTP status: 500")
})
it("should fail validation when model does not exist", async () => {
// Mock successful /api/tags call with different models
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
models: [{ name: "llama2:latest" }, { name: "mistral:latest" }],
}),
} as Response),
)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe(
"Model 'nomic-embed-text' not found. Available models: llama2:latest, mistral:latest",
)
})
it("should fail validation when model exists but doesn't support embeddings", async () => {
// Mock successful /api/tags call
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
models: [{ name: "nomic-embed-text" }],
}),
} as Response),
)
// Mock failed /api/embed test call
mockFetch.mockImplementationOnce(() =>
Promise.resolve({
ok: false,
status: 400,
} as Response),
)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Model 'nomic-embed-text' is not embedding capable")
})
it("should handle ECONNREFUSED errors", async () => {
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Connection to Ollama timed out at http://localhost:11434")
})
it("should handle ENOTFOUND errors", async () => {
mockFetch.mockRejectedValueOnce(new Error("ENOTFOUND"))
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Ollama host not found: http://localhost:11434")
})
it("should handle generic network errors", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network timeout"))
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Network timeout")
})
})
})

View file

@ -882,4 +882,133 @@ describe("OpenAICompatibleEmbedder", () => {
expect(mockCreate).toHaveBeenCalled()
})
})
describe("validateConfiguration", () => {
let embedder: OpenAICompatibleEmbedder
let mockFetch: MockedFunction<typeof fetch>
beforeEach(() => {
vitest.clearAllMocks()
// Reset and re-assign the global fetch mock
global.fetch = vitest.fn()
mockFetch = global.fetch as MockedFunction<typeof fetch>
})
it("should validate successfully with valid configuration and base URL", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
const mockResponse = {
data: [{ embedding: [0.1, 0.2, 0.3] }],
usage: { prompt_tokens: 2, total_tokens: 2 },
}
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: testModelId,
encoding_format: "base64",
})
})
it("should validate successfully with full endpoint URL", async () => {
const fullUrl = "https://api.example.com/v1/embeddings"
embedder = new OpenAICompatibleEmbedder(fullUrl, testApiKey, testModelId)
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: [{ embedding: [0.1, 0.2, 0.3] }],
usage: { prompt_tokens: 2, total_tokens: 2 },
}),
text: async () => "",
} as any)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(mockFetch).toHaveBeenCalledWith(
fullUrl,
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Authorization: `Bearer ${testApiKey}`,
}),
}),
)
})
it("should fail validation with authentication error", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
const authError = new Error("Invalid API key")
;(authError as any).status = 401
mockEmbeddingsCreate.mockRejectedValue(authError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.authenticationFailed")
})
it("should fail validation with connection error", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
const connectionError = new Error("ECONNREFUSED")
mockEmbeddingsCreate.mockRejectedValue(connectionError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.connectionFailed")
})
it("should fail validation with invalid endpoint for full URL", async () => {
const fullUrl = "https://api.example.com/v1/embeddings"
embedder = new OpenAICompatibleEmbedder(fullUrl, testApiKey, testModelId)
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ error: "Not found" }),
text: async () => "Not found",
} as any)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.invalidEndpoint")
})
it("should fail validation with rate limit error", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
const rateLimitError = new Error("Rate limit exceeded")
;(rateLimitError as any).status = 429
mockEmbeddingsCreate.mockRejectedValue(rateLimitError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.serviceUnavailable")
})
it("should fail validation with generic error", async () => {
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
const genericError = new Error("Unknown error")
;(genericError as any).status = 500
mockEmbeddingsCreate.mockRejectedValue(genericError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.configurationError")
})
})
})

View file

@ -464,4 +464,66 @@ describe("OpenAiEmbedder", () => {
})
})
})
describe("validateConfiguration", () => {
it("should validate successfully with valid configuration", async () => {
const mockResponse = {
data: [{ embedding: [0.1, 0.2, 0.3] }],
usage: { prompt_tokens: 2, total_tokens: 2 },
}
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(true)
expect(result.error).toBeUndefined()
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: "text-embedding-3-small",
})
})
it("should fail validation with authentication error", async () => {
const authError = new Error("Invalid API key")
;(authError as any).status = 401
mockEmbeddingsCreate.mockRejectedValue(authError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.authenticationFailed")
})
it("should fail validation with rate limit error", async () => {
const rateLimitError = new Error("Rate limit exceeded")
;(rateLimitError as any).status = 429
mockEmbeddingsCreate.mockRejectedValue(rateLimitError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.serviceUnavailable")
})
it("should fail validation with connection error", async () => {
const connectionError = new Error("ECONNREFUSED")
mockEmbeddingsCreate.mockRejectedValue(connectionError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.connectionFailed")
})
it("should fail validation with generic error", async () => {
const genericError = new Error("Unknown error")
;(genericError as any).status = 500
mockEmbeddingsCreate.mockRejectedValue(genericError)
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("embeddings:validation.configurationError")
})
})
})

View file

@ -46,6 +46,16 @@ export class GeminiEmbedder implements IEmbedder {
return this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL)
}
/**
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Gemini since we're using Gemini's base URL
return this.openAICompatibleEmbedder.validateConfiguration()
}
/**
* Returns information about this embedder
*/

View file

@ -3,6 +3,7 @@ import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces"
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
import { withValidationErrorHandling } from "../shared/validation-helpers"
/**
* Implements the IEmbedder interface using a local Ollama instance.
@ -101,6 +102,127 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
}
}
/**
* Validates the Ollama embedder configuration by checking service availability and model existence
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
return withValidationErrorHandling(
async () => {
// First check if Ollama service is running by trying to list models
const modelsUrl = `${this.baseUrl}/api/tags`
// Add timeout to prevent indefinite hanging
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout
const modelsResponse = await fetch(modelsUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!modelsResponse.ok) {
if (modelsResponse.status === 404) {
return {
valid: false,
error: t("embeddings:errors.ollama.serviceNotRunning", { baseUrl: this.baseUrl }),
}
}
return {
valid: false,
error: t("embeddings:errors.ollama.serviceUnavailable", {
baseUrl: this.baseUrl,
status: modelsResponse.status,
}),
}
}
// Check if the specific model exists
const modelsData = await modelsResponse.json()
const models = modelsData.models || []
// Check both with and without :latest suffix
const modelExists = models.some((m: any) => {
const modelName = m.name || ""
return (
modelName === this.defaultModelId ||
modelName === `${this.defaultModelId}:latest` ||
modelName === this.defaultModelId.replace(":latest", "")
)
})
if (!modelExists) {
const availableModels = models.map((m: any) => m.name).join(", ")
return {
valid: false,
error: t("embeddings:errors.ollama.modelNotFound", {
model: this.defaultModelId,
availableModels,
}),
}
}
// Try a test embedding to ensure the model works for embeddings
const testUrl = `${this.baseUrl}/api/embed`
// Add timeout for test request too
const testController = new AbortController()
const testTimeoutId = setTimeout(() => testController.abort(), 5000)
const testResponse = await fetch(testUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: this.defaultModelId,
input: ["test"],
}),
signal: testController.signal,
})
clearTimeout(testTimeoutId)
if (!testResponse.ok) {
return {
valid: false,
error: t("embeddings:errors.ollama.modelNotEmbedding", { model: this.defaultModelId }),
}
}
return { valid: true }
},
"ollama",
{
beforeStandardHandling: (error: any) => {
// Handle Ollama-specific connection errors
if (error?.message === "ECONNREFUSED") {
return {
valid: false,
error: t("embeddings:errors.ollama.connectionTimeout", { baseUrl: this.baseUrl }),
}
} else if (error?.message === "ENOTFOUND") {
return {
valid: false,
error: t("embeddings:errors.ollama.hostNotFound", { baseUrl: this.baseUrl }),
}
} else if (error?.name === "AbortError") {
// Handle timeout
return {
valid: false,
error: t("embeddings:errors.ollama.connectionTimeout", { baseUrl: this.baseUrl }),
}
}
// Let standard handling take over
return undefined
},
},
)
}
get embedderInfo(): EmbedderInfo {
return {
name: "ollama",

View file

@ -8,6 +8,7 @@ import {
} from "../constants"
import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers"
interface EmbeddingItem {
embedding: string | number[]
@ -26,12 +27,6 @@ interface OpenAIEmbeddingResponse {
* OpenAI Compatible implementation of the embedder interface with batching and rate limiting.
* This embedder allows using any OpenAI-compatible API endpoint by specifying a custom baseURL.
*/
interface HttpError extends Error {
status?: number
response?: {
status?: number
}
}
export class OpenAICompatibleEmbedder implements IEmbedder {
private embeddingsClient: OpenAI
@ -201,14 +196,31 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
}),
})
if (!response.ok) {
const errorText = await response.text()
const error = new Error(`HTTP ${response.status}: ${errorText}`) as HttpError
error.status = response.status
if (!response || !response.ok) {
const status = response?.status || 0
let errorText = "No response"
try {
if (response && typeof response.text === "function") {
errorText = await response.text()
} else if (response) {
errorText = `Error ${status}`
}
} catch {
// Ignore text parsing errors
errorText = `Error ${status}`
}
const error = new Error(`HTTP ${status}: ${errorText}`) as HttpError
error.status = status || response?.status || 0
throw error
}
return await response.json()
try {
return await response.json()
} catch (e) {
const error = new Error(`Failed to parse response JSON`) as HttpError
error.status = response.status
throw error
}
}
/**
@ -272,11 +284,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
},
}
} catch (error) {
const httpError = error as HttpError
const isRateLimitError = httpError?.status === 429
const hasMoreAttempts = attempts < MAX_RETRIES - 1
if (isRateLimitError && hasMoreAttempts) {
// Check if it's a rate limit error
const httpError = error as HttpError
if (httpError?.status === 429 && hasMoreAttempts) {
const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempts)
console.warn(
t("embeddings:rateLimitRetry", {
@ -292,37 +304,50 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
// Log the error for debugging
console.error(`OpenAI Compatible embedder error (attempt ${attempts + 1}/${MAX_RETRIES}):`, error)
// Provide more context in the error message using robust error extraction
let errorMessage = t("embeddings:unknownError")
if (httpError?.message) {
errorMessage = httpError.message
} else if (typeof error === "string") {
errorMessage = error
} else if (error && typeof error === "object" && "toString" in error) {
try {
errorMessage = String(error)
} catch {
errorMessage = t("embeddings:unknownError")
}
}
const statusCode = httpError?.status || httpError?.response?.status
if (statusCode === 401) {
throw new Error(t("embeddings:authenticationFailed"))
} else if (statusCode) {
throw new Error(
t("embeddings:failedWithStatus", { attempts: MAX_RETRIES, statusCode, errorMessage }),
)
} else {
throw new Error(t("embeddings:failedWithError", { attempts: MAX_RETRIES, errorMessage }))
}
// Format and throw the error
throw formatEmbeddingError(error, MAX_RETRIES)
}
}
throw new Error(t("embeddings:failedMaxAttempts", { attempts: MAX_RETRIES }))
}
/**
* Validates the OpenAI-compatible embedder configuration by testing endpoint connectivity and API key
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
return withValidationErrorHandling(async () => {
// Test with a minimal embedding request
const testTexts = ["test"]
const modelToUse = this.defaultModelId
let response: OpenAIEmbeddingResponse
if (this.isFullUrl) {
// Test direct HTTP request for full endpoint URLs
response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse)
} else {
// Test using OpenAI SDK for base URLs
response = (await this.embeddingsClient.embeddings.create({
input: testTexts,
model: modelToUse,
encoding_format: "base64",
})) as OpenAIEmbeddingResponse
}
// Check if we got a valid response
if (!response?.data || response.data.length === 0) {
return {
valid: false,
error: "embeddings:validation.invalidResponse",
}
}
return { valid: true }
}, "openai-compatible")
}
/**
* Returns information about this embedder
*/

View file

@ -10,6 +10,7 @@ import {
} from "../constants"
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers"
/**
* OpenAI implementation of the embedder interface with batching and rate limiting
@ -138,10 +139,11 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
},
}
} catch (error: any) {
const isRateLimitError = error?.status === 429
const hasMoreAttempts = attempts < MAX_RETRIES - 1
if (isRateLimitError && hasMoreAttempts) {
// Check if it's a rate limit error
const httpError = error as HttpError
if (httpError?.status === 429 && hasMoreAttempts) {
const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempts)
console.warn(
t("embeddings:rateLimitRetry", {
@ -157,37 +159,38 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
// Log the error for debugging
console.error(`OpenAI embedder error (attempt ${attempts + 1}/${MAX_RETRIES}):`, error)
// Provide more context in the error message using robust error extraction
let errorMessage = "Unknown error"
if (error?.message) {
errorMessage = error.message
} else if (typeof error === "string") {
errorMessage = error
} else if (error && typeof error.toString === "function") {
try {
errorMessage = error.toString()
} catch {
errorMessage = "Unknown error"
}
}
const statusCode = error?.status || error?.response?.status
if (statusCode === 401) {
throw new Error(t("embeddings:authenticationFailed"))
} else if (statusCode) {
throw new Error(
t("embeddings:failedWithStatus", { attempts: MAX_RETRIES, statusCode, errorMessage }),
)
} else {
throw new Error(t("embeddings:failedWithError", { attempts: MAX_RETRIES, errorMessage }))
}
// Format and throw the error
throw formatEmbeddingError(error, MAX_RETRIES)
}
}
throw new Error(t("embeddings:failedMaxAttempts", { attempts: MAX_RETRIES }))
}
/**
* Validates the OpenAI embedder configuration by attempting a minimal embedding request
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
return withValidationErrorHandling(async () => {
// Test with a minimal embedding request
const response = await this.embeddingsClient.embeddings.create({
input: ["test"],
model: this.defaultModelId,
})
// Check if we got a valid response
if (!response.data || response.data.length === 0) {
return {
valid: false,
error: t("embeddings:openai.invalidResponseFormat"),
}
}
return { valid: true }
}, "openai")
}
get embedderInfo(): EmbedderInfo {
return {
name: "openai",

View file

@ -10,6 +10,13 @@ export interface IEmbedder {
* @returns Promise resolving to an EmbeddingResponse
*/
createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse>
/**
* Validates the embedder configuration by testing connectivity and credentials.
* @returns Promise resolving to validation result with success status and optional error message
*/
validateConfiguration(): Promise<{ valid: boolean; error?: string }>
get embedderInfo(): EmbedderInfo
}

View file

@ -118,7 +118,14 @@ export class CodeIndexManager {
return { requiresRestart }
}
// 3. CacheManager Initialization
// 3. Check if workspace is available
const workspacePath = getWorkspacePath()
if (!workspacePath) {
this._stateManager.setSystemState("Standby", "No workspace folder open")
return { requiresRestart }
}
// 4. CacheManager Initialization
if (!this._cacheManager) {
this._cacheManager = new CacheManager(this.context, this.workspacePath)
await this._cacheManager.initialize()
@ -215,6 +222,9 @@ export class CodeIndexManager {
if (this._orchestrator) {
this.stopWatcher()
}
// Clear existing services to ensure clean state
this._orchestrator = undefined
this._searchService = undefined
// (Re)Initialize service factory
this._serviceFactory = new CodeIndexServiceFactory(
@ -224,7 +234,14 @@ export class CodeIndexManager {
)
const ignoreInstance = ignore()
const ignorePath = path.join(getWorkspacePath(), ".gitignore")
const workspacePath = getWorkspacePath()
if (!workspacePath) {
this._stateManager.setSystemState("Standby", "")
return
}
const ignorePath = path.join(workspacePath, ".gitignore")
try {
const content = await fs.readFile(ignorePath, "utf8")
ignoreInstance.add(content)
@ -241,6 +258,17 @@ export class CodeIndexManager {
ignoreInstance,
)
// 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")
}
// (Re)Initialize orchestrator
this._orchestrator = new CodeIndexOrchestrator(
this._configManager!,
@ -259,6 +287,9 @@ export class CodeIndexManager {
embedder,
vectorStore,
)
// Clear any error state after successful recreation
this._stateManager.setSystemState("Standby", "")
}
/**
@ -274,13 +305,16 @@ export class CodeIndexManager {
const isFeatureEnabled = this.isFeatureEnabled
const isFeatureConfigured = this.isFeatureConfigured
// If configuration changes require a restart and the manager is initialized, restart the service
if (requiresRestart && isFeatureEnabled && isFeatureConfigured && this.isInitialized) {
// Recreate services with new configuration
await this._recreateServices()
// Start indexing with new services
this.startIndexing()
if (requiresRestart && isFeatureEnabled && isFeatureConfigured) {
try {
// Recreate services with new configuration
await this._recreateServices()
} catch (error) {
// Error state already set in _recreateServices
console.error("Failed to recreate services:", error)
// Re-throw the error so the caller knows validation failed
throw error
}
}
}
}

View file

@ -164,6 +164,31 @@ export class CodeIndexOrchestrator {
}
}
// Check for partial failures - if a significant portion of blocks failed
const failureRate = (cumulativeBlocksFoundSoFar - cumulativeBlocksIndexed) / cumulativeBlocksFoundSoFar
if (batchErrors.length > 0 && failureRate > 0.1) {
// More than 10% of blocks failed to index
const firstError = batchErrors[0]
throw new Error(
`Indexing partially failed: Only ${cumulativeBlocksIndexed} of ${cumulativeBlocksFoundSoFar} blocks were indexed. ${firstError.message}`,
)
}
// CRITICAL: If there were ANY batch errors and NO blocks were successfully indexed,
// this is a complete failure regardless of the failure rate calculation
if (batchErrors.length > 0 && cumulativeBlocksIndexed === 0) {
const firstError = batchErrors[0]
throw new Error(`Indexing failed completely: ${firstError.message}`)
}
// Final sanity check: If we found blocks but indexed none and somehow no errors were reported,
// this is still a failure
if (cumulativeBlocksFoundSoFar > 0 && cumulativeBlocksIndexed === 0) {
throw new Error(
"Indexing failed: No code blocks were successfully indexed despite finding files to process. This indicates a critical embedder failure.",
)
}
await this._startWatcher()
this.stateManager.setSystemState("Indexed", "File watcher started.")

View file

@ -66,6 +66,23 @@ export class CodeIndexServiceFactory {
throw new Error(`Invalid embedder type configured: ${config.embedderProvider}`)
}
/**
* Validates an embedder instance to ensure it's properly configured.
* @param embedder The embedder instance to validate
* @returns Promise resolving to validation result
*/
public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> {
try {
return await embedder.validateConfiguration()
} catch (error) {
// If validation throws an exception, preserve the original error message
return {
valid: false,
error: error instanceof Error ? error.message : "embeddings:validation.configurationError",
}
}
}
/**
* Creates a vector store instance using the current configuration.
*/

View file

@ -0,0 +1,187 @@
import { t } from "../../../i18n"
import { serializeError } from "serialize-error"
/**
* HTTP error interface for embedder errors
*/
export interface HttpError extends Error {
status?: number
response?: {
status?: number
}
}
/**
* Common error types that can occur during embedder validation
*/
export interface ValidationError {
status?: number
message?: string
name?: string
code?: string
}
/**
* Maps HTTP status codes to appropriate error messages
*/
export function getErrorMessageForStatus(status: number | undefined, embedderType: string): string | undefined {
switch (status) {
case 401:
case 403:
return "embeddings:validation.authenticationFailed"
case 404:
return embedderType === "openai"
? "embeddings:validation.modelNotAvailable"
: "embeddings:validation.invalidEndpoint"
case 429:
return "embeddings:validation.serviceUnavailable"
default:
if (status && status >= 400 && status < 600) {
return "embeddings:validation.configurationError"
}
return undefined
}
}
/**
* Extracts status code from various error formats
*/
export function extractStatusCode(error: any): number | undefined {
// Direct status property
if (error?.status) return error.status
// Response status property
if (error?.response?.status) return error.response.status
// Extract from error message (e.g., "HTTP 404: Not Found")
if (error?.message) {
const match = error.message.match(/HTTP (\d+):/)
if (match) {
return parseInt(match[1], 10)
}
}
// Use serialize-error as fallback for complex objects
const serialized = serializeError(error)
if (serialized?.status) return serialized.status
if (serialized?.response?.status) return serialized.response.status
return undefined
}
/**
* Extracts error message from various error formats
*/
export function extractErrorMessage(error: any): string {
if (error?.message) {
return error.message
}
if (typeof error === "string") {
return error
}
if (error && typeof error === "object" && "toString" in error) {
try {
return String(error)
} catch {
return "Unknown error"
}
}
// Use serialize-error as fallback for complex objects
const serialized = serializeError(error)
if (serialized?.message) {
return serialized.message
}
return "Unknown error"
}
/**
* Standard validation error handler for embedder configuration validation
* Returns a consistent error response based on the error type
*/
export function handleValidationError(
error: any,
embedderType: string,
customHandlers?: {
beforeStandardHandling?: (error: any) => { valid: boolean; error: string } | undefined
},
): { valid: boolean; error: string } {
// Serialize the error to ensure we have access to all properties
const serializedError = serializeError(error)
// Allow custom handling first (pass original error for backward compatibility)
if (customHandlers?.beforeStandardHandling) {
const customResult = customHandlers.beforeStandardHandling(error)
if (customResult) return customResult
}
// Extract status code and error message from serialized error
const statusCode = extractStatusCode(serializedError)
const errorMessage = extractErrorMessage(serializedError)
// Check for status-based errors first
const statusError = getErrorMessageForStatus(statusCode, embedderType)
if (statusError) {
return { valid: false, error: statusError }
}
// Check for connection errors
if (errorMessage) {
if (
errorMessage.includes("ENOTFOUND") ||
errorMessage.includes("ECONNREFUSED") ||
errorMessage.includes("ETIMEDOUT") ||
errorMessage === "AbortError" ||
errorMessage.includes("HTTP 0:") ||
errorMessage === "No response"
) {
return { valid: false, error: "embeddings:validation.connectionFailed" }
}
if (errorMessage.includes("Failed to parse response JSON")) {
return { valid: false, error: "embeddings:validation.invalidResponse" }
}
}
// For generic errors, preserve the original error message if it's not a standard one
if (errorMessage && errorMessage !== "Unknown error") {
return { valid: false, error: errorMessage }
}
// Fallback to generic error
return { valid: false, error: "embeddings:validation.configurationError" }
}
/**
* Wraps an async validation function with standard error handling
*/
export async function withValidationErrorHandling<T extends { valid: boolean; error?: string }>(
validationFn: () => Promise<T>,
embedderType: string,
customHandlers?: Parameters<typeof handleValidationError>[2],
): Promise<{ valid: boolean; error?: string }> {
try {
return await validationFn()
} catch (error) {
return handleValidationError(error, embedderType, customHandlers)
}
}
/**
* Formats an embedding error message based on the error type and context
*/
export function formatEmbeddingError(error: any, maxRetries: number): Error {
const errorMessage = extractErrorMessage(error)
const statusCode = extractStatusCode(error)
if (statusCode === 401) {
return new Error(t("embeddings:authenticationFailed"))
} else if (statusCode) {
return new Error(t("embeddings:failedWithStatus", { attempts: maxRetries, statusCode, errorMessage }))
} else {
return new Error(t("embeddings:failedWithError", { attempts: maxRetries, errorMessage }))
}
}

View file

@ -110,6 +110,7 @@ export interface WebviewMessage {
| "enhancedPrompt"
| "draggedImages"
| "deleteMessage"
| "submitEditedMessage"
| "terminalOutputLineLimit"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
@ -193,6 +194,7 @@ export interface WebviewMessage {
| "saveCodeIndexSettingsAtomic"
| "requestCodeIndexSecretStatus"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
disabled?: boolean
dataUri?: string

View file

@ -13,7 +13,6 @@
"noImplicitReturns": true,
"noUnusedLocals": false,
"resolveJsonModule": true,
"rootDir": ".",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,

View file

@ -111,6 +111,8 @@ export const ChatRowContent = ({
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
const { copyWithFeedback } = useCopyToClipboard()
// Memoized callback to prevent re-renders caused by inline arrow functions
@ -118,6 +120,31 @@ export const ChatRowContent = ({
onToggleExpand(message.ts)
}, [onToggleExpand, message.ts])
// Handle edit button click
const handleEditClick = useCallback(() => {
setIsEditing(true)
setEditedContent(message.text || "")
// Edit mode is now handled entirely in the frontend
// No need to notify the backend
}, [message.text])
// Handle cancel edit
const handleCancelEdit = useCallback(() => {
setIsEditing(false)
setEditedContent(message.text || "")
}, [message.text])
// Handle save edit
const handleSaveEdit = useCallback(() => {
setIsEditing(false)
// Send edited message to backend
vscode.postMessage({
type: "submitEditedMessage",
value: message.ts,
editedMessageContent: editedContent,
})
}, [message.ts, editedContent])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
const info = safeJsonParse<ClineApiReqInfo>(message.text)
@ -1001,23 +1028,56 @@ export const ChatRowContent = ({
case "user_feedback":
return (
<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
<div className="flex justify-between">
<div className="flex-grow px-2 py-1 wrap-anywhere">
<Mention text={message.text} withShadow />
{isEditing ? (
<div className="flex flex-col gap-2 p-2">
<textarea
className="w-full p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded-xs"
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
rows={5}
autoFocus
/>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="sm" onClick={handleCancelEdit}>
{t("chat:cancel.title")}
</Button>
<Button variant="default" size="sm" onClick={handleSaveEdit}>
{t("chat:save.title")}
</Button>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="shrink-0"
disabled={isStreaming}
onClick={(e) => {
e.stopPropagation()
vscode.postMessage({ type: "deleteMessage", value: message.ts })
}}>
<span className="codicon codicon-trash" />
</Button>
</div>
{message.images && message.images.length > 0 && (
) : (
<div className="flex justify-between">
<div className="flex-grow px-2 py-1 wrap-anywhere">
<Mention text={message.text} withShadow />
</div>
<div className="flex">
<Button
variant="ghost"
size="icon"
className="shrink-0"
disabled={isStreaming}
onClick={(e) => {
e.stopPropagation()
handleEditClick()
}}>
<span className="codicon codicon-edit" />
</Button>
<Button
variant="ghost"
size="icon"
className="shrink-0"
disabled={isStreaming}
onClick={(e) => {
e.stopPropagation()
vscode.postMessage({ type: "deleteMessage", value: message.ts })
}}>
<span className="codicon codicon-trash" />
</Button>
</div>
</div>
)}
{!isEditing && message.images && message.images.length > 0 && (
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
)}
</div>

View file

@ -445,13 +445,15 @@ describe("ChatTextArea", () => {
})
})
it("should navigate to previous prompt on arrow up", () => {
it("should navigate to previous prompt on arrow up when cursor is at beginning", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Ensure cursor is at the beginning
textarea.setSelectionRange(0, 0)
// Simulate arrow up key press
fireEvent.keyDown(textarea, { key: "ArrowUp" })
@ -755,6 +757,86 @@ describe("ChatTextArea", () => {
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Message 2")
})
it("should not navigate history with arrow up when cursor is not at beginning", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Some text here" />,
)
const textarea = container.querySelector("textarea")!
// Set cursor to middle of text (not at beginning)
textarea.setSelectionRange(5, 5)
// Clear any calls from initial render
setInputValue.mockClear()
// Simulate arrow up key press
fireEvent.keyDown(textarea, { key: "ArrowUp" })
// Should not navigate history, allowing default behavior (move cursor to start)
expect(setInputValue).not.toHaveBeenCalled()
})
it("should navigate history with arrow up when cursor is at beginning", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Some text here" />,
)
const textarea = container.querySelector("textarea")!
// Set cursor to beginning of text
textarea.setSelectionRange(0, 0)
// Clear any calls from initial render
setInputValue.mockClear()
// Simulate arrow up key press
fireEvent.keyDown(textarea, { key: "ArrowUp" })
// Should navigate to history since cursor is at beginning
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
})
it("should navigate history with Command+Up when cursor is at beginning", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Some text here" />,
)
const textarea = container.querySelector("textarea")!
// Set cursor to beginning of text
textarea.setSelectionRange(0, 0)
// Clear any calls from initial render
setInputValue.mockClear()
// Simulate Command+Up key press
fireEvent.keyDown(textarea, { key: "ArrowUp", metaKey: true })
// Should navigate to history since cursor is at beginning (same as regular Up)
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
})
it("should not navigate history with Command+Up when cursor is not at beginning", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Some text here" />,
)
const textarea = container.querySelector("textarea")!
// Set cursor to middle of text (not at beginning)
textarea.setSelectionRange(5, 5)
// Clear any calls from initial render
setInputValue.mockClear()
// Simulate Command+Up key press
fireEvent.keyDown(textarea, { key: "ArrowUp", metaKey: true })
// Should not navigate history, allowing default behavior (same as regular Up)
expect(setInputValue).not.toHaveBeenCalled()
})
})
})

View file

@ -139,32 +139,8 @@ export const usePromptHistory = ({
const isAtBeginning = selectionStart === 0 && selectionEnd === 0
const isAtEnd = selectionStart === value.length && selectionEnd === value.length
// Check for modifier keys (Alt or Cmd/Ctrl)
const hasModifier = event.altKey || event.metaKey || event.ctrlKey
// Handle explicit history navigation with Alt+Up/Down
if (hasModifier && (event.key === "ArrowUp" || event.key === "ArrowDown")) {
event.preventDefault()
if (event.key === "ArrowUp") {
// Save current input if starting navigation
if (historyIndex === -1) {
setTempInput(inputValue)
}
return navigateToHistory(historyIndex + 1, textarea, "start")
} else {
// ArrowDown
if (historyIndex > 0) {
return navigateToHistory(historyIndex - 1, textarea, "end")
} else if (historyIndex === 0) {
returnToCurrentInput(textarea, "end")
return true
}
}
}
// Handle smart navigation without modifiers
if (!hasSelection && !hasModifier) {
// Handle smart navigation
if (!hasSelection) {
// Only navigate history with UP if cursor is at the very beginning
if (event.key === "ArrowUp" && isAtBeginning) {
event.preventDefault()