diff --git a/ui/src/features/agent/wizardTurnReport.ts b/ui/src/features/agent/wizardTurnReport.ts index 7f8b45a5c..92dc894c0 100644 --- a/ui/src/features/agent/wizardTurnReport.ts +++ b/ui/src/features/agent/wizardTurnReport.ts @@ -43,7 +43,8 @@ type Translate = (key: string, options?: Record) => string /** Conservative presentation policy, not an authorization or execution classifier. */ function allowsExplanation(request: string): boolean { - const text = request.trim().replace(/^[¿¡]+/, '') + // JS `\b` is ASCII-only. Fold accents so "Qué" / "por qué" keep a word boundary. + const text = request.trim().replace(/^[¿¡]+/, '').normalize('NFD').replace(/[\u0300-\u036f]/g, '') if (/^(?:hola|hello|hi|gracias|thanks)[\s!.]*$/i.test(text)) return true // An informational prefix does not erase a later imperative in a mixed turn. if (/(?:[,;.!?\n]|\b(?:and|then|also|y|luego|despu[eé]s))\s*(?:(?:please|por favor)[,\s]+)?(?:create|generate|make|update|delete|remove|add|save|export|start|retry|run|open|select|crea\w*|genera\w*|haz\w*|actualiza\w*|elimina\w*|borra\w*|a[nñ]ade\w*|guarda\w*|exporta\w*|inicia\w*|reintenta\w*|ejecuta\w*|abre|selecciona\w*)\b/i.test(text)) return false diff --git a/ui/tests/wizardTurnReport.test.ts b/ui/tests/wizardTurnReport.test.ts index 253a44a71..743777ec5 100644 --- a/ui/tests/wizardTurnReport.test.ts +++ b/ui/tests/wizardTurnReport.test.ts @@ -101,6 +101,11 @@ test('informational conversation is preserved while navigation requires its own const reply = 'Collections group existing assets.' assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, 'What are collections?'), reply) assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, '¿Cómo funcionan las colecciones?'), reply) + // Accented qué/por qué used to fail JS `\b` and show a false empty-turn receipt. + assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, 'Qué son las colecciones?'), reply) + assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, '¿Qué son las colecciones?'), reply) + assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, 'Por qué no aparecen las colecciones?'), reply) + assert.equal(formatWizardTurnReply({ reply, actions: [] }, [], t, '¿Por qué falló la generación?'), reply) assert.match(formatWizardTurnReply({ reply, actions: [{ type: 'open_tab', tab: 'workspaces' }] }, [], t), /No action was executed/) }) @@ -110,7 +115,7 @@ test('empty and omitted actions never claim creation in response to an action re for (const request of ['Create Nightwatch using my settings.', 'Can you create a collection?', 'Crea un proyecto Nightwatch.', 'Hola, crea un proyecto Nightwatch.', 'Hi, create Nightwatch.', 'Can you explain collections and create Nightwatch?', 'What are collections? Create one named Nightwatch.', - 'Explica las colecciones y crea Nightwatch.', '']) { + 'Explica las colecciones y crea Nightwatch.', 'Qué son las colecciones? Crea una llamada Nightwatch.', '']) { const reply = formatWizardTurnReply(turn, [], t, request) assert.match(reply, /No action was executed/) assert.doesNotMatch(reply, /invented-999|Created/)