mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(chat): render tool-call failure explanations in View-processing timeline (#4254, frontend) (#4413)
This commit is contained in:
@@ -35,6 +35,34 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'الخزنة موجودة على مضيف النواة.',
|
||||
'crossHostVault.message':
|
||||
'يتم تخزين خزنة الذاكرة هذه على مضيف openhuman-core ({os}). لا يمكن فتحها أو عرضها إلا على ذلك الجهاز، وليس من هذا الجهاز.',
|
||||
'conversations.toolFailure.whyLabel': 'لماذا',
|
||||
'conversations.toolFailure.nextLabel': 'ما الذي يجب فعله بعد ذلك',
|
||||
'conversations.toolFailure.missingPermission.cause': 'لا يملك OpenHuman الإذن للقيام بهذا بعد.',
|
||||
'conversations.toolFailure.missingPermission.next': 'امنح الإذن المطلوب ثم حاول مرة أخرى.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'التطبيق أو البرنامج اللازم لهذا الإجراء غير متوفر.',
|
||||
'conversations.toolFailure.missingApp.next': 'ثبّت التطبيق أو افتحه ثم حاول مرة أخرى.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'إحدى الخدمات التي يحتاجها OpenHuman غير متوفرة مؤقتًا.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'سيحاول OpenHuman مرة أخرى قريبًا — لا حاجة لأي إجراء.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'بيانات تسجيل الدخول المحفوظة مفقودة أو لم تعد صالحة.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'سجّل الدخول مجددًا أو حدّث بيانات الاعتماد ثم حاول مرة أخرى.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'هذا الإجراء محظور بواسطة إعدادات الأمان الخاصة بك.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'اسمح به من الإعدادات → وصول الوكيل إذا أردت تشغيله.',
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'تعذّر على OpenHuman الوصول إلى نموذج الذكاء الاصطناعي.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'تحقّق من اتصالك أو إعدادات النموذج؛ سيعيد OpenHuman المحاولة.',
|
||||
'conversations.toolFailure.timeout.cause': 'استغرق الإجراء وقتًا طويلاً وتم إيقافه.',
|
||||
'conversations.toolFailure.timeout.next': 'سيعيد OpenHuman المحاولة، أو يمكنك إعادتها يدويًا.',
|
||||
'conversations.toolFailure.unknown.cause': 'حدث خطأ ما في هذا الإجراء.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'حاول مرة أخرى؛ وإذا استمر الفشل، شغّل التشخيص من الإعدادات.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'شارك ملاحظاتك',
|
||||
'feedback.board': 'لوحة الملاحظات',
|
||||
|
||||
@@ -37,6 +37,36 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'ভল্টটি কোর হোস্টে রয়েছে।',
|
||||
'crossHostVault.message':
|
||||
'এই মেমরি ভল্টটি openhuman-core হোস্টে ({os}) সংরক্ষিত আছে। এটি কেবল সেই মেশিনেই খোলা বা দেখানো যায়, এই ডিভাইস থেকে নয়।',
|
||||
'conversations.toolFailure.whyLabel': 'কেন',
|
||||
'conversations.toolFailure.nextLabel': 'এরপর কী করবেন',
|
||||
'conversations.toolFailure.missingPermission.cause': 'OpenHuman-এর এখনও এটি করার অনুমতি নেই।',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'প্রয়োজনীয় অনুমতি দিন, তারপর আবার চেষ্টা করুন।',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'এই কাজের জন্য প্রয়োজনীয় অ্যাপ বা প্রোগ্রামটি উপলব্ধ নেই।',
|
||||
'conversations.toolFailure.missingApp.next':
|
||||
'অ্যাপটি ইনস্টল করুন বা খুলুন, তারপর আবার চেষ্টা করুন।',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'OpenHuman-এর প্রয়োজনীয় একটি পরিষেবা সাময়িকভাবে অনুপলব্ধ।',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman শীঘ্রই আবার চেষ্টা করবে — কোনো পদক্ষেপের প্রয়োজন নেই।',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'সংরক্ষিত সাইন-ইন তথ্য অনুপস্থিত বা আর বৈধ নয়।',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'আবার সাইন ইন করুন বা শংসাপত্র আপডেট করুন, তারপর আবার চেষ্টা করুন।',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'এই কাজটি আপনার নিরাপত্তা সেটিংস দ্বারা অবরুদ্ধ।',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'আপনি চালাতে চাইলে সেটিংস → এজেন্ট অ্যাক্সেস-এ এটি অনুমতি দিন।',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman AI মডেলে পৌঁছাতে পারেনি।',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'আপনার সংযোগ বা মডেল সেটিংস পরীক্ষা করুন; OpenHuman আবার চেষ্টা করবে।',
|
||||
'conversations.toolFailure.timeout.cause': 'কাজটি অনেক বেশি সময় নেওয়ায় থামিয়ে দেওয়া হয়েছে।',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman আবার চেষ্টা করবে, অথবা আপনি নিজে আবার চেষ্টা করতে পারেন।',
|
||||
'conversations.toolFailure.unknown.cause': 'এই কাজটিতে কিছু ভুল হয়েছে।',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'আবার চেষ্টা করুন; বারবার ব্যর্থ হলে সেটিংস থেকে ডায়াগনস্টিকস চালান।',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'মতামত দিন',
|
||||
'feedback.board': 'মতামত বোর্ড',
|
||||
|
||||
@@ -39,6 +39,38 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'Der Vault liegt auf dem Core-Host.',
|
||||
'crossHostVault.message':
|
||||
'Dieser Memory-Vault wird auf dem openhuman-core-Host ({os}) gespeichert. Er kann nur auf diesem Rechner geöffnet oder angezeigt werden, nicht von diesem Gerät.',
|
||||
'conversations.toolFailure.whyLabel': 'Warum',
|
||||
'conversations.toolFailure.nextLabel': 'Nächste Schritte',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman hat noch keine Berechtigung, dies zu tun.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Erteile die benötigte Berechtigung und versuche es erneut.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'Die für diese Aktion benötigte App oder das Programm ist nicht verfügbar.',
|
||||
'conversations.toolFailure.missingApp.next':
|
||||
'Installiere oder öffne die App und versuche es erneut.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Ein von OpenHuman benötigter Dienst ist vorübergehend nicht verfügbar.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman versucht es in Kürze erneut – nichts weiter nötig.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Die gespeicherten Anmeldedaten fehlen oder sind nicht mehr gültig.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Melde dich erneut an oder aktualisiere die Anmeldedaten und versuche es erneut.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Diese Aktion wird durch deine Sicherheitseinstellungen blockiert.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Erlaube sie unter Einstellungen → Agentenzugriff, wenn sie ausgeführt werden soll.',
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'OpenHuman konnte das KI-Modell nicht erreichen.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Prüfe deine Verbindung oder Modelleinstellungen; OpenHuman versucht es erneut.',
|
||||
'conversations.toolFailure.timeout.cause': 'Die Aktion hat zu lange gedauert und wurde gestoppt.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman versucht es erneut, oder du kannst es manuell wiederholen.',
|
||||
'conversations.toolFailure.unknown.cause': 'Bei dieser Aktion ist etwas schiefgelaufen.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Versuche es erneut; wenn es weiterhin fehlschlägt, führe die Diagnose in den Einstellungen aus.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Feedback geben',
|
||||
'feedback.board': 'Feedback-Board',
|
||||
|
||||
@@ -3813,6 +3813,37 @@ const en: TranslationMap = {
|
||||
'conversations.subagent.cancel': 'Cancel task',
|
||||
'conversations.subagent.cancelling': 'Cancelling…',
|
||||
'conversations.subagent.cancelFailed': "Couldn't cancel the task. Try again.",
|
||||
// Tool-failure explanation surfaced under a failed step in "View processing" (#4254).
|
||||
'conversations.toolFailure.whyLabel': 'Why',
|
||||
'conversations.toolFailure.nextLabel': 'What to do next',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
"OpenHuman doesn't have permission to do this yet.",
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Grant the permission it needs, then try again.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
"The app or program needed for this action isn't available.",
|
||||
'conversations.toolFailure.missingApp.next': 'Install or open the app, then try again.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'A service OpenHuman needs is temporarily unavailable.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman will try again shortly — no action needed.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'The saved sign-in details are missing or no longer valid.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Sign in again or update the credentials, then try again.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'This action is blocked by your safety settings.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Allow it in Settings → Agent access if you want it to run.',
|
||||
'conversations.toolFailure.modelConnection.cause': "OpenHuman couldn't reach the AI model.",
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Check your connection or model settings; OpenHuman will retry.',
|
||||
'conversations.toolFailure.timeout.cause': 'The action took too long and was stopped.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman will try again, or you can retry it manually.',
|
||||
'conversations.toolFailure.unknown.cause': 'Something went wrong with this action.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Try again; if it keeps failing, run diagnostics from Settings.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'conversations.backgroundTasks.titleWithCount': 'Background tasks ({count})',
|
||||
'conversations.backgroundTasks.running': '{count} running',
|
||||
|
||||
@@ -38,6 +38,37 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'El vault está en el host del core.',
|
||||
'crossHostVault.message':
|
||||
'Este vault de memoria se almacena en el host de openhuman-core ({os}). Solo se puede abrir o mostrar en esa máquina, no desde este dispositivo.',
|
||||
'conversations.toolFailure.whyLabel': 'Por qué',
|
||||
'conversations.toolFailure.nextLabel': 'Qué hacer a continuación',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman aún no tiene permiso para hacer esto.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Concede el permiso que necesita y vuelve a intentarlo.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'La app o el programa necesario para esta acción no está disponible.',
|
||||
'conversations.toolFailure.missingApp.next': 'Instala o abre la app y vuelve a intentarlo.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Un servicio que OpenHuman necesita no está disponible temporalmente.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman lo intentará de nuevo en breve; no hace falta hacer nada.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Los datos de inicio de sesión guardados faltan o ya no son válidos.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Inicia sesión de nuevo o actualiza las credenciales y vuelve a intentarlo.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Esta acción está bloqueada por tu configuración de seguridad.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Permítela en Configuración → Acceso del agente si quieres que se ejecute.',
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'OpenHuman no pudo conectarse con el modelo de IA.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Comprueba tu conexión o la configuración del modelo; OpenHuman lo reintentará.',
|
||||
'conversations.toolFailure.timeout.cause': 'La acción tardó demasiado y se detuvo.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman lo intentará de nuevo, o puedes reintentarlo manualmente.',
|
||||
'conversations.toolFailure.unknown.cause': 'Algo salió mal con esta acción.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Inténtalo de nuevo; si sigue fallando, ejecuta el diagnóstico desde Configuración.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Compartir opiniones',
|
||||
'feedback.board': 'Tablero de opiniones',
|
||||
|
||||
@@ -39,6 +39,36 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': "Le coffre se trouve sur l'hôte du cœur.",
|
||||
'crossHostVault.message':
|
||||
"Ce coffre de mémoire est stocké sur l'hôte openhuman-core ({os}). Il ne peut être ouvert ou affiché que sur cette machine, pas depuis cet appareil.",
|
||||
'conversations.toolFailure.whyLabel': 'Pourquoi',
|
||||
'conversations.toolFailure.nextLabel': 'Que faire ensuite',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
"OpenHuman n'a pas encore l'autorisation de faire cela.",
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
"Accordez l'autorisation nécessaire, puis réessayez.",
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
"L'application ou le programme requis pour cette action n'est pas disponible.",
|
||||
'conversations.toolFailure.missingApp.next': "Installez ou ouvrez l'application, puis réessayez.",
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Un service dont OpenHuman a besoin est temporairement indisponible.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman réessaiera bientôt — aucune action nécessaire.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Les identifiants enregistrés sont manquants ou ne sont plus valides.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Reconnectez-vous ou mettez à jour les identifiants, puis réessayez.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Cette action est bloquée par vos paramètres de sécurité.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
"Autorisez-la dans Paramètres → Accès de l'agent si vous voulez qu'elle s'exécute.",
|
||||
'conversations.toolFailure.modelConnection.cause': "OpenHuman n'a pas pu joindre le modèle d'IA.",
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Vérifiez votre connexion ou les paramètres du modèle ; OpenHuman réessaiera.',
|
||||
'conversations.toolFailure.timeout.cause': "L'action a pris trop de temps et a été arrêtée.",
|
||||
'conversations.toolFailure.timeout.next':
|
||||
"OpenHuman réessaiera, ou vous pouvez relancer l'action manuellement.",
|
||||
'conversations.toolFailure.unknown.cause': 'Un problème est survenu avec cette action.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
"Réessayez ; si l'échec persiste, lancez le diagnostic depuis les Paramètres.",
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Donner mon avis',
|
||||
'feedback.board': 'Tableau des suggestions',
|
||||
|
||||
@@ -37,6 +37,35 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'वॉल्ट कोर होस्ट पर है।',
|
||||
'crossHostVault.message':
|
||||
'यह मेमोरी वॉल्ट openhuman-core होस्ट ({os}) पर संग्रहीत है। इसे केवल उसी मशीन पर खोला या दिखाया जा सकता है, इस डिवाइस से नहीं।',
|
||||
'conversations.toolFailure.whyLabel': 'क्यों',
|
||||
'conversations.toolFailure.nextLabel': 'आगे क्या करें',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman के पास अभी यह करने की अनुमति नहीं है।',
|
||||
'conversations.toolFailure.missingPermission.next': 'आवश्यक अनुमति दें, फिर दोबारा प्रयास करें।',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'इस कार्य के लिए आवश्यक ऐप या प्रोग्राम उपलब्ध नहीं है।',
|
||||
'conversations.toolFailure.missingApp.next': 'ऐप इंस्टॉल करें या खोलें, फिर दोबारा प्रयास करें।',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'OpenHuman को जिस सेवा की आवश्यकता है वह अस्थायी रूप से अनुपलब्ध है।',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman शीघ्र ही फिर से प्रयास करेगा — कोई कार्रवाई आवश्यक नहीं।',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'सहेजी गई साइन-इन जानकारी गायब है या अब मान्य नहीं है।',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'फिर से साइन इन करें या क्रेडेंशियल अपडेट करें, फिर दोबारा प्रयास करें।',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'यह कार्य आपकी सुरक्षा सेटिंग्स द्वारा अवरुद्ध है।',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'यदि आप इसे चलाना चाहते हैं तो इसे सेटिंग्स → एजेंट एक्सेस में अनुमति दें।',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman AI मॉडल तक नहीं पहुँच सका।',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'अपना कनेक्शन या मॉडल सेटिंग्स जाँचें; OpenHuman फिर से प्रयास करेगा।',
|
||||
'conversations.toolFailure.timeout.cause': 'कार्य में बहुत अधिक समय लगा और इसे रोक दिया गया।',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman फिर से प्रयास करेगा, या आप इसे मैन्युअल रूप से दोबारा कर सकते हैं।',
|
||||
'conversations.toolFailure.unknown.cause': 'इस कार्य में कुछ गड़बड़ हो गई।',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'दोबारा प्रयास करें; यदि यह बार-बार विफल हो, तो सेटिंग्स से डायग्नोस्टिक्स चलाएँ।',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'फ़ीडबैक साझा करें',
|
||||
'feedback.board': 'फ़ीडबैक बोर्ड',
|
||||
|
||||
@@ -38,6 +38,36 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'Vault berada di host core.',
|
||||
'crossHostVault.message':
|
||||
'Vault memori ini disimpan di host openhuman-core ({os}). Hanya dapat dibuka atau ditampilkan di mesin tersebut, bukan dari perangkat ini.',
|
||||
'conversations.toolFailure.whyLabel': 'Mengapa',
|
||||
'conversations.toolFailure.nextLabel': 'Yang harus dilakukan',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman belum memiliki izin untuk melakukan ini.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Berikan izin yang diperlukan, lalu coba lagi.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'Aplikasi atau program yang diperlukan untuk tindakan ini tidak tersedia.',
|
||||
'conversations.toolFailure.missingApp.next': 'Instal atau buka aplikasinya, lalu coba lagi.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Layanan yang dibutuhkan OpenHuman sedang tidak tersedia untuk sementara.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman akan mencoba lagi sebentar lagi — tidak perlu tindakan apa pun.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Detail masuk yang tersimpan tidak ada atau tidak lagi valid.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Masuk kembali atau perbarui kredensial, lalu coba lagi.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Tindakan ini diblokir oleh pengaturan keamanan Anda.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Izinkan di Pengaturan → Akses agen jika Anda ingin menjalankannya.',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman tidak dapat menjangkau model AI.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Periksa koneksi atau pengaturan model Anda; OpenHuman akan mencoba lagi.',
|
||||
'conversations.toolFailure.timeout.cause': 'Tindakan memakan waktu terlalu lama dan dihentikan.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman akan mencoba lagi, atau Anda dapat mengulanginya secara manual.',
|
||||
'conversations.toolFailure.unknown.cause': 'Terjadi kesalahan pada tindakan ini.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Coba lagi; jika terus gagal, jalankan diagnostik dari Pengaturan.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Bagikan masukan',
|
||||
'feedback.board': 'Papan masukan',
|
||||
|
||||
@@ -37,6 +37,38 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': "Il vault è sull'host del core.",
|
||||
'crossHostVault.message':
|
||||
"Questo vault di memoria è archiviato sull'host openhuman-core ({os}). Può essere aperto o mostrato solo su quella macchina, non da questo dispositivo.",
|
||||
'conversations.toolFailure.whyLabel': 'Perché',
|
||||
'conversations.toolFailure.nextLabel': 'Cosa fare ora',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman non ha ancora il permesso di farlo.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Concedi il permesso necessario, poi riprova.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
"L'app o il programma necessario per questa azione non è disponibile.",
|
||||
'conversations.toolFailure.missingApp.next': "Installa o apri l'app, poi riprova.",
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Un servizio necessario a OpenHuman è temporaneamente non disponibile.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman riproverà a breve — non serve fare nulla.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'I dati di accesso salvati mancano o non sono più validi.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Accedi di nuovo o aggiorna le credenziali, poi riprova.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Questa azione è bloccata dalle tue impostazioni di sicurezza.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
"Consentila in Impostazioni → Accesso dell'agente se vuoi che venga eseguita.",
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'OpenHuman non è riuscito a raggiungere il modello di IA.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Controlla la connessione o le impostazioni del modello; OpenHuman riproverà.',
|
||||
'conversations.toolFailure.timeout.cause':
|
||||
"L'azione ha richiesto troppo tempo ed è stata interrotta.",
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman riproverà, oppure puoi riprovare manualmente.',
|
||||
'conversations.toolFailure.unknown.cause': 'Qualcosa è andato storto con questa azione.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Riprova; se continua a fallire, esegui la diagnostica dalle Impostazioni.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Condividi feedback',
|
||||
'feedback.board': 'Bacheca dei feedback',
|
||||
|
||||
@@ -35,6 +35,34 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': '보관소가 코어 호스트에 있습니다.',
|
||||
'crossHostVault.message':
|
||||
'이 메모리 보관소는 openhuman-core 호스트({os})에 저장되어 있습니다. 해당 컴퓨터에서만 열거나 표시할 수 있으며 이 기기에서는 불가능합니다.',
|
||||
'conversations.toolFailure.whyLabel': '이유',
|
||||
'conversations.toolFailure.nextLabel': '다음 할 일',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman에 아직 이 작업을 수행할 권한이 없습니다.',
|
||||
'conversations.toolFailure.missingPermission.next': '필요한 권한을 부여한 후 다시 시도하세요.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'이 작업에 필요한 앱 또는 프로그램을 사용할 수 없습니다.',
|
||||
'conversations.toolFailure.missingApp.next': '앱을 설치하거나 연 후 다시 시도하세요.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'OpenHuman에 필요한 서비스를 일시적으로 사용할 수 없습니다.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman이 곧 다시 시도합니다 — 별도의 조치가 필요하지 않습니다.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'저장된 로그인 정보가 없거나 더 이상 유효하지 않습니다.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'다시 로그인하거나 자격 증명을 업데이트한 후 다시 시도하세요.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause': '이 작업은 보안 설정에 의해 차단되었습니다.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'실행하려면 설정 → 에이전트 액세스에서 허용하세요.',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman이 AI 모델에 연결하지 못했습니다.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'연결 또는 모델 설정을 확인하세요. OpenHuman이 다시 시도합니다.',
|
||||
'conversations.toolFailure.timeout.cause': '작업이 너무 오래 걸려 중지되었습니다.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman이 다시 시도하거나 수동으로 다시 실행할 수 있습니다.',
|
||||
'conversations.toolFailure.unknown.cause': '이 작업에서 문제가 발생했습니다.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'다시 시도하세요. 계속 실패하면 설정에서 진단을 실행하세요.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': '피드백 보내기',
|
||||
'feedback.board': '피드백 보드',
|
||||
|
||||
@@ -39,6 +39,38 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'Skarbiec znajduje się na hoście rdzenia.',
|
||||
'crossHostVault.message':
|
||||
'Ten skarbiec pamięci jest przechowywany na hoście openhuman-core ({os}). Można go otworzyć lub pokazać tylko na tym komputerze, a nie z tego urządzenia.',
|
||||
'conversations.toolFailure.whyLabel': 'Dlaczego',
|
||||
'conversations.toolFailure.nextLabel': 'Co dalej',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'OpenHuman nie ma jeszcze uprawnień, aby to zrobić.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Nadaj wymagane uprawnienie i spróbuj ponownie.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'Aplikacja lub program potrzebny do tej czynności jest niedostępny.',
|
||||
'conversations.toolFailure.missingApp.next':
|
||||
'Zainstaluj lub otwórz aplikację i spróbuj ponownie.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Usługa, której potrzebuje OpenHuman, jest chwilowo niedostępna.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman spróbuje ponownie za chwilę — nie musisz nic robić.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Zapisane dane logowania są niedostępne lub już nieważne.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Zaloguj się ponownie lub zaktualizuj dane logowania, a potem spróbuj jeszcze raz.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Ta czynność jest zablokowana przez Twoje ustawienia bezpieczeństwa.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Zezwól na nią w Ustawienia → Dostęp agenta, jeśli ma zostać wykonana.',
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'OpenHuman nie mógł połączyć się z modelem AI.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Sprawdź połączenie lub ustawienia modelu; OpenHuman spróbuje ponownie.',
|
||||
'conversations.toolFailure.timeout.cause': 'Czynność trwała zbyt długo i została zatrzymana.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman spróbuje ponownie lub możesz powtórzyć ją ręcznie.',
|
||||
'conversations.toolFailure.unknown.cause': 'Coś poszło nie tak z tą czynnością.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Spróbuj ponownie; jeśli nadal się nie udaje, uruchom diagnostykę w Ustawieniach.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Podziel się opinią',
|
||||
'feedback.board': 'Tablica opinii',
|
||||
|
||||
@@ -37,6 +37,37 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'O vault está no host do core.',
|
||||
'crossHostVault.message':
|
||||
'Este vault de memória fica armazenado no host openhuman-core ({os}). Só pode ser aberto ou exibido nessa máquina, não a partir deste dispositivo.',
|
||||
'conversations.toolFailure.whyLabel': 'Por quê',
|
||||
'conversations.toolFailure.nextLabel': 'O que fazer a seguir',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'O OpenHuman ainda não tem permissão para fazer isso.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Conceda a permissão necessária e tente novamente.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'O app ou programa necessário para esta ação não está disponível.',
|
||||
'conversations.toolFailure.missingApp.next': 'Instale ou abra o app e tente novamente.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Um serviço de que o OpenHuman precisa está temporariamente indisponível.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'O OpenHuman tentará de novo em breve — nenhuma ação necessária.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Os dados de login salvos estão ausentes ou não são mais válidos.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Faça login novamente ou atualize as credenciais e tente de novo.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Esta ação está bloqueada pelas suas configurações de segurança.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Permita-a em Configurações → Acesso do agente se quiser que ela seja executada.',
|
||||
'conversations.toolFailure.modelConnection.cause':
|
||||
'O OpenHuman não conseguiu acessar o modelo de IA.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Verifique sua conexão ou as configurações do modelo; o OpenHuman tentará novamente.',
|
||||
'conversations.toolFailure.timeout.cause': 'A ação demorou demais e foi interrompida.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'O OpenHuman tentará novamente, ou você pode tentar manualmente.',
|
||||
'conversations.toolFailure.unknown.cause': 'Algo deu errado com esta ação.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Tente novamente; se continuar falhando, execute o diagnóstico nas Configurações.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Enviar feedback',
|
||||
'feedback.board': 'Quadro de feedback',
|
||||
|
||||
@@ -39,6 +39,38 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': 'Хранилище находится на хосте ядра.',
|
||||
'crossHostVault.message':
|
||||
'Это хранилище памяти размещено на хосте openhuman-core ({os}). Его можно открыть или показать только на той машине, но не с этого устройства.',
|
||||
'conversations.toolFailure.whyLabel': 'Почему',
|
||||
'conversations.toolFailure.nextLabel': 'Что делать дальше',
|
||||
'conversations.toolFailure.missingPermission.cause':
|
||||
'У OpenHuman пока нет разрешения на это действие.',
|
||||
'conversations.toolFailure.missingPermission.next':
|
||||
'Предоставьте нужное разрешение и повторите попытку.',
|
||||
'conversations.toolFailure.missingApp.cause':
|
||||
'Приложение или программа, необходимые для этого действия, недоступны.',
|
||||
'conversations.toolFailure.missingApp.next':
|
||||
'Установите или откройте приложение и повторите попытку.',
|
||||
'conversations.toolFailure.serviceUnavailable.cause':
|
||||
'Служба, необходимая OpenHuman, временно недоступна.',
|
||||
'conversations.toolFailure.serviceUnavailable.next':
|
||||
'OpenHuman скоро повторит попытку — ничего делать не нужно.',
|
||||
'conversations.toolFailure.badCredentials.cause':
|
||||
'Сохранённые данные для входа отсутствуют или больше недействительны.',
|
||||
'conversations.toolFailure.badCredentials.next':
|
||||
'Войдите заново или обновите учётные данные, затем повторите попытку.',
|
||||
'conversations.toolFailure.blockedByPolicy.cause':
|
||||
'Это действие заблокировано вашими настройками безопасности.',
|
||||
'conversations.toolFailure.blockedByPolicy.next':
|
||||
'Разрешите его в разделе Настройки → Доступ агента, если хотите, чтобы оно выполнилось.',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman не удалось связаться с ИИ-моделью.',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'Проверьте подключение или настройки модели; OpenHuman повторит попытку.',
|
||||
'conversations.toolFailure.timeout.cause':
|
||||
'Действие заняло слишком много времени и было остановлено.',
|
||||
'conversations.toolFailure.timeout.next':
|
||||
'OpenHuman повторит попытку, или вы можете повторить её вручную.',
|
||||
'conversations.toolFailure.unknown.cause': 'С этим действием что-то пошло не так.',
|
||||
'conversations.toolFailure.unknown.next':
|
||||
'Повторите попытку; если ошибка повторяется, запустите диагностику в Настройках.',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': 'Поделиться отзывом',
|
||||
'feedback.board': 'Доска отзывов',
|
||||
|
||||
@@ -33,6 +33,25 @@ const messages: TranslationMap = {
|
||||
'crossHostVault.title': '记忆库位于核心主机上。',
|
||||
'crossHostVault.message':
|
||||
'此记忆库存储在 openhuman-core 主机({os})上。只能在该机器上打开或显示,无法从本设备访问。',
|
||||
'conversations.toolFailure.whyLabel': '原因',
|
||||
'conversations.toolFailure.nextLabel': '接下来该怎么做',
|
||||
'conversations.toolFailure.missingPermission.cause': 'OpenHuman 目前还没有执行此操作的权限。',
|
||||
'conversations.toolFailure.missingPermission.next': '授予所需权限,然后重试。',
|
||||
'conversations.toolFailure.missingApp.cause': '此操作所需的应用或程序不可用。',
|
||||
'conversations.toolFailure.missingApp.next': '安装或打开该应用,然后重试。',
|
||||
'conversations.toolFailure.serviceUnavailable.cause': 'OpenHuman 需要的某项服务暂时不可用。',
|
||||
'conversations.toolFailure.serviceUnavailable.next': 'OpenHuman 稍后会重试——无需任何操作。',
|
||||
'conversations.toolFailure.badCredentials.cause': '保存的登录信息缺失或已失效。',
|
||||
'conversations.toolFailure.badCredentials.next': '请重新登录或更新凭据,然后重试。',
|
||||
'conversations.toolFailure.blockedByPolicy.cause': '此操作已被你的安全设置阻止。',
|
||||
'conversations.toolFailure.blockedByPolicy.next': '如果希望执行,请在“设置 → 代理访问”中允许它。',
|
||||
'conversations.toolFailure.modelConnection.cause': 'OpenHuman 无法连接到 AI 模型。',
|
||||
'conversations.toolFailure.modelConnection.next':
|
||||
'请检查你的网络连接或模型设置;OpenHuman 将会重试。',
|
||||
'conversations.toolFailure.timeout.cause': '操作耗时过长,已被停止。',
|
||||
'conversations.toolFailure.timeout.next': 'OpenHuman 会重试,你也可以手动重试。',
|
||||
'conversations.toolFailure.unknown.cause': '此操作出现了问题。',
|
||||
'conversations.toolFailure.unknown.next': '请重试;如果持续失败,请在设置中运行诊断。',
|
||||
'conversations.backgroundTasks.title': 'Background tasks',
|
||||
'nav.feedback': '分享反馈',
|
||||
'feedback.board': '反馈板',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ToolTimelineEntry } from '../../../store/chatRuntimeSlice';
|
||||
import { ProcessingTranscriptView } from './ProcessingTranscriptView';
|
||||
|
||||
// Echo i18n: return the fallback when one is provided (so localized copy keys
|
||||
// resolve to the English source we pass), otherwise the key itself. This lets
|
||||
// us assert on the human strings carried on the failure payload.
|
||||
vi.mock('../../../lib/i18n/I18nContext', () => ({
|
||||
useT: () => ({ t: (key: string, fallback?: string) => fallback ?? key, locale: 'en' }),
|
||||
}));
|
||||
|
||||
function failedEntry(overrides: Partial<ToolTimelineEntry> = {}): ToolTimelineEntry {
|
||||
return {
|
||||
id: 'call-1',
|
||||
name: 'read_file',
|
||||
round: 1,
|
||||
status: 'error',
|
||||
failure: {
|
||||
class: 'MissingPermission',
|
||||
category: 'NeedsUserConfirmation',
|
||||
recoverable: false,
|
||||
causePlain: 'No permission yet.',
|
||||
nextAction: 'Grant it, then retry.',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ProcessingTranscriptView tool failure explanation', () => {
|
||||
it('renders the cause + next-action under a failed tool row', () => {
|
||||
// Empty transcript → single tool group over all entries (legacy path).
|
||||
render(<ProcessingTranscriptView transcript={[]} entries={[failedEntry()]} />);
|
||||
|
||||
const failure = screen.getByTestId('processing-tool-failure');
|
||||
expect(failure).toBeTruthy();
|
||||
expect(failure.textContent).toContain('No permission yet.');
|
||||
expect(failure.textContent).toContain('Grant it, then retry.');
|
||||
});
|
||||
|
||||
it('falls back to the plain English copy for an unrecognized class', () => {
|
||||
render(
|
||||
<ProcessingTranscriptView
|
||||
transcript={[]}
|
||||
entries={[
|
||||
failedEntry({
|
||||
failure: {
|
||||
class: 'SomethingBrandNew',
|
||||
category: 'Recoverable',
|
||||
recoverable: true,
|
||||
causePlain: 'Mystery cause.',
|
||||
nextAction: 'Mystery next.',
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const failure = screen.getByTestId('processing-tool-failure');
|
||||
expect(failure.textContent).toContain('Mystery cause.');
|
||||
expect(failure.textContent).toContain('Mystery next.');
|
||||
});
|
||||
|
||||
it('does not render the failure block for a successful entry', () => {
|
||||
render(
|
||||
<ProcessingTranscriptView
|
||||
transcript={[]}
|
||||
entries={[failedEntry({ status: 'success', failure: undefined })]}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('processing-tool-failure')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import type {
|
||||
ProcessingTranscriptItem,
|
||||
ToolFailureExplanation,
|
||||
ToolTimelineEntry,
|
||||
ToolTimelineEntryStatus,
|
||||
} from '../../../store/chatRuntimeSlice';
|
||||
@@ -112,6 +113,27 @@ function ToolGroupBlock({ summary, entries }: { summary: string; entries: ToolTi
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The 8 failure classes the UI has localized copy for (#4254), keyed by the
|
||||
* camelCase form of the wire's PascalCase `class`. Any class not in this set
|
||||
* falls back to the English `causePlain` / `nextAction` carried on the payload.
|
||||
*/
|
||||
const LOCALIZED_FAILURE_CLASSES: ReadonlySet<string> = new Set([
|
||||
'missingPermission',
|
||||
'missingApp',
|
||||
'serviceUnavailable',
|
||||
'badCredentials',
|
||||
'blockedByPolicy',
|
||||
'modelConnection',
|
||||
'timeout',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
/** Lowercase the first character: `MissingPermission` → `missingPermission`. */
|
||||
function toCamelClass(cls: string): string {
|
||||
return cls.length > 0 ? cls[0].toLowerCase() + cls.slice(1) : cls;
|
||||
}
|
||||
|
||||
/** One tool step: type icon + human sentence + contextual detail chip. */
|
||||
function ToolRow({ entry }: { entry: ToolTimelineEntry }) {
|
||||
const { title, detail } = formatTimelineEntry(entry);
|
||||
@@ -127,11 +149,44 @@ function ToolRow({ entry }: { entry: ToolTimelineEntry }) {
|
||||
{detail}
|
||||
</span>
|
||||
) : null}
|
||||
{entry.status === 'error' && entry.failure ? (
|
||||
<ToolFailureLines failure={entry.failure} />
|
||||
) : null}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "why + what to do next" pair rendered under a failed tool row (#4254).
|
||||
* Copy is resolved by failure class from i18n, falling back to the English
|
||||
* `causePlain` / `nextAction` carried on the wire when the class is one the UI
|
||||
* hasn't localized. The failure line is coral to match the error glyph.
|
||||
*/
|
||||
function ToolFailureLines({ failure }: { failure: ToolFailureExplanation }) {
|
||||
const { t } = useT();
|
||||
const camel = toCamelClass(failure.class);
|
||||
const known = LOCALIZED_FAILURE_CLASSES.has(camel);
|
||||
const cause = known
|
||||
? t(`conversations.toolFailure.${camel}.cause`, failure.causePlain)
|
||||
: failure.causePlain;
|
||||
const next = known
|
||||
? t(`conversations.toolFailure.${camel}.next`, failure.nextAction)
|
||||
: failure.nextAction;
|
||||
return (
|
||||
<span
|
||||
data-testid="processing-tool-failure"
|
||||
className="mt-1 flex flex-col gap-0.5 text-[11px] leading-snug">
|
||||
<span className="text-coral-600 dark:text-coral-300">
|
||||
<span className="font-semibold">{t('conversations.toolFailure.whyLabel')}:</span> {cause}
|
||||
</span>
|
||||
<span className="text-content-muted">
|
||||
<span className="font-semibold">{t('conversations.toolFailure.nextLabel')}:</span> {next}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Compact terminal status glyph for the group's "Done" line. */
|
||||
function StatusGlyph({ status }: { status: ToolTimelineEntryStatus }) {
|
||||
if (status === 'error') {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
clearStreamingAssistantForThread,
|
||||
endInferenceTurn,
|
||||
markInferenceTurnStreaming,
|
||||
parseToolFailure,
|
||||
recordChatTurnUsage,
|
||||
recordProcessingTool,
|
||||
recordSubagentTranscriptTool,
|
||||
@@ -586,6 +587,11 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
)
|
||||
return;
|
||||
|
||||
// On failure, parse the optional structured explanation (#4254) once so
|
||||
// both the id-match and name/round-fallback paths can attach it. A
|
||||
// successful result clears any stale failure carried on the row.
|
||||
const failure = event.success ? undefined : parseToolFailure(event.failure);
|
||||
|
||||
const existing = store.getState().chatRuntime.toolTimelineByThread[event.thread_id] ?? [];
|
||||
if (existing.length > 0) {
|
||||
const nextEntries = [...existing];
|
||||
@@ -597,6 +603,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
nextEntries[idx] = {
|
||||
...nextEntries[idx],
|
||||
status: event.success ? 'success' : 'error',
|
||||
failure,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
@@ -610,7 +617,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
entry.name === event.tool_name &&
|
||||
entry.round === event.round
|
||||
) {
|
||||
nextEntries[i] = { ...entry, status: event.success ? 'success' : 'error' };
|
||||
nextEntries[i] = { ...entry, status: event.success ? 'success' : 'error', failure };
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,13 @@ export interface ChatToolResultEvent {
|
||||
round: number;
|
||||
/** Matches the id on the corresponding {@link ChatToolCallEvent}. */
|
||||
tool_call_id?: string;
|
||||
/**
|
||||
* Optional structured failure explanation, present only when `success` is
|
||||
* false (#4254). Raw snake_case wire object (`class`, `category`,
|
||||
* `recoverable`, `cause_plain`, `next_action`); parsed defensively via
|
||||
* `parseToolFailure` before it reaches the store.
|
||||
*/
|
||||
failure?: unknown;
|
||||
}
|
||||
|
||||
/** One sub-agent's token/cost contribution within a turn (hover breakdown). */
|
||||
@@ -1168,9 +1175,8 @@ export interface ChatSendParams {
|
||||
/**
|
||||
* Queue mode for concurrent messages. When a turn is already in
|
||||
* flight: `steer` injects at the next iteration boundary, `followup`
|
||||
* queues for after the turn, `collect` adds as context, and `parallel`
|
||||
* starts a forked same-thread turn. `interrupt` (default) aborts the
|
||||
* running turn.
|
||||
* queues for after the turn, `collect` adds as context. `interrupt`
|
||||
* (default) aborts the running turn.
|
||||
*/
|
||||
queueMode?: QueueMode | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseToolFailure } from './chatRuntimeSlice';
|
||||
|
||||
describe('parseToolFailure', () => {
|
||||
it('parses a well-formed snake_case wire object into camelCase', () => {
|
||||
const parsed = parseToolFailure({
|
||||
class: 'MissingPermission',
|
||||
category: 'NeedsUserConfirmation',
|
||||
recoverable: false,
|
||||
cause_plain: "OpenHuman doesn't have permission to do this yet.",
|
||||
next_action: 'Grant the permission it needs, then try again.',
|
||||
});
|
||||
expect(parsed).toEqual({
|
||||
class: 'MissingPermission',
|
||||
category: 'NeedsUserConfirmation',
|
||||
recoverable: false,
|
||||
causePlain: "OpenHuman doesn't have permission to do this yet.",
|
||||
nextAction: 'Grant the permission it needs, then try again.',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts camelCase keys (persisted round-trip)', () => {
|
||||
const parsed = parseToolFailure({
|
||||
class: 'Timeout',
|
||||
category: 'Recoverable',
|
||||
recoverable: true,
|
||||
causePlain: 'Took too long.',
|
||||
nextAction: 'Retry.',
|
||||
});
|
||||
expect(parsed?.causePlain).toBe('Took too long.');
|
||||
expect(parsed?.nextAction).toBe('Retry.');
|
||||
expect(parsed?.recoverable).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults recoverable to false when absent or non-boolean', () => {
|
||||
const parsed = parseToolFailure({
|
||||
class: 'Unknown',
|
||||
category: 'Recoverable',
|
||||
cause_plain: 'Oops.',
|
||||
next_action: 'Try again.',
|
||||
});
|
||||
expect(parsed?.recoverable).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['null', null],
|
||||
['undefined', undefined],
|
||||
['a string', 'MissingPermission'],
|
||||
['a number', 42],
|
||||
['an empty object', {}],
|
||||
['missing next_action', { class: 'X', category: 'Y', cause_plain: 'c' }],
|
||||
['non-string class', { class: 1, category: 'Y', cause_plain: 'c', next_action: 'n' }],
|
||||
])('returns undefined for garbage input (%s)', (_label, input) => {
|
||||
expect(parseToolFailure(input)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -176,6 +176,59 @@ export interface SubagentToolCallEntry {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable explanation for a FAILED tool call (#4254). Carried on the
|
||||
* tool-completion socket event (optional `failure` object, snake_case on the
|
||||
* wire) and surfaced in the "View processing" timeline as a "why" + "what to
|
||||
* do next" pair. `class`/`category` come from the core's failure taxonomy;
|
||||
* `causePlain`/`nextAction` are English fallbacks used when the class is not
|
||||
* one the UI has localized copy for.
|
||||
*/
|
||||
export interface ToolFailureExplanation {
|
||||
/** PascalCase failure class, e.g. `MissingPermission`, `Timeout`, `Unknown`. */
|
||||
class: string;
|
||||
/** `Recoverable` | `BlockedByPolicy` | `NeedsUserConfirmation`. */
|
||||
category: string;
|
||||
/** Whether the core considers the failure automatically recoverable. */
|
||||
recoverable: boolean;
|
||||
/** English fallback cause copy (used when `class` is unrecognized). */
|
||||
causePlain: string;
|
||||
/** English fallback next-action copy (used when `class` is unrecognized). */
|
||||
nextAction: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defensively parse an incoming `failure` object from a tool-completion socket
|
||||
* payload (snake_case wire) or a persisted entry (camelCase) into a
|
||||
* {@link ToolFailureExplanation}. Returns `undefined` for anything that is not
|
||||
* a well-formed failure object so a malformed/partial payload never corrupts a
|
||||
* timeline entry.
|
||||
*/
|
||||
export function parseToolFailure(raw: unknown): ToolFailureExplanation | undefined {
|
||||
if (!raw || typeof raw !== 'object') return undefined;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const cls = obj.class;
|
||||
const category = obj.category;
|
||||
// Accept both wire (snake_case) and persisted (camelCase) key spellings.
|
||||
const causePlain = obj.cause_plain ?? obj.causePlain;
|
||||
const nextAction = obj.next_action ?? obj.nextAction;
|
||||
if (
|
||||
typeof cls !== 'string' ||
|
||||
typeof category !== 'string' ||
|
||||
typeof causePlain !== 'string' ||
|
||||
typeof nextAction !== 'string'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
class: cls,
|
||||
category,
|
||||
recoverable: typeof obj.recoverable === 'boolean' ? obj.recoverable : false,
|
||||
causePlain,
|
||||
nextAction,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolTimelineEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -192,6 +245,13 @@ export interface ToolTimelineEntry {
|
||||
* rows and for legacy snapshots emitted by older cores.
|
||||
*/
|
||||
subagent?: SubagentActivity;
|
||||
/**
|
||||
* Human-readable failure explanation for an `error` row (#4254). Parsed from
|
||||
* the tool-completion socket event's optional `failure` object; absent on
|
||||
* successful/running rows and on legacy snapshots. Preserved through the
|
||||
* persisted round-trip so a reloaded failed turn keeps its explanation.
|
||||
*/
|
||||
failure?: ToolFailureExplanation;
|
||||
}
|
||||
|
||||
export interface StreamingAssistantState {
|
||||
@@ -669,6 +729,9 @@ function toolTimelineFromPersisted(entry: PersistedToolTimelineEntry): ToolTimel
|
||||
detail: entry.detail,
|
||||
sourceToolName: entry.sourceToolName,
|
||||
subagent: entry.subagent ? subagentActivityFromPersisted(entry.subagent) : undefined,
|
||||
// Carry a persisted failure explanation across the round-trip (#4254). The
|
||||
// shared parser tolerates both camelCase (persisted) and snake_case (wire).
|
||||
failure: parseToolFailure(entry.failure),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,20 @@ export interface PersistedSubagentActivity {
|
||||
transcript?: PersistedSubagentTranscriptItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable failure explanation persisted alongside a FAILED tool row
|
||||
* (#4254). Mirrors the socket `failure` object; carried in the snapshot so a
|
||||
* settled/reloaded turn keeps its "why + what to do next" explanation. Absent
|
||||
* on successful rows and on snapshots written before this field.
|
||||
*/
|
||||
export interface PersistedToolFailure {
|
||||
class: string;
|
||||
category: string;
|
||||
recoverable: boolean;
|
||||
causePlain: string;
|
||||
nextAction: string;
|
||||
}
|
||||
|
||||
export interface PersistedToolTimelineEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -128,6 +142,7 @@ export interface PersistedToolTimelineEntry {
|
||||
detail?: string;
|
||||
sourceToolName?: string;
|
||||
subagent?: PersistedSubagentActivity;
|
||||
failure?: PersistedToolFailure;
|
||||
}
|
||||
|
||||
export interface PersistedTurnState {
|
||||
|
||||
Reference in New Issue
Block a user