diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index fbde7a5175..097a65f113 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -9,6 +9,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': 'لوحة الملاحظات', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 3f9ed538d5..74d3607cc8 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -10,6 +10,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': 'মতামত বোর্ড', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 8d2ba158d3..35b81c61bc 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -9,6 +9,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', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 7111c751bc..7fd766e50d 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -3786,6 +3786,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', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index ca549b36cd..6d7333c055 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 5caf845b29..55cfe4fe12 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 7db36e6675..a325ecdab0 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -9,6 +9,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': 'फ़ीडबैक बोर्ड', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 76c8941be3..7913894dc2 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 613a8cbca8..4b71e774a6 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 0a5c175f42..80fc2ffe03 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -9,6 +9,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': '피드백 보드', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 971aa31720..d9b6d628e5 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 15755004db..4a7a4a66db 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -10,6 +10,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', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index b7a17b96e8..1119d5ac5c 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -10,6 +10,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': 'Доска отзывов', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 78453ad354..fcc569122b 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -9,6 +9,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': '反馈板', diff --git a/app/src/pages/conversations/components/ProcessingTranscriptView.test.tsx b/app/src/pages/conversations/components/ProcessingTranscriptView.test.tsx new file mode 100644 index 0000000000..94a6926b77 --- /dev/null +++ b/app/src/pages/conversations/components/ProcessingTranscriptView.test.tsx @@ -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 { + 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(); + + 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( + + ); + + 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( + + ); + expect(screen.queryByTestId('processing-tool-failure')).toBeNull(); + }); +}); diff --git a/app/src/pages/conversations/components/ProcessingTranscriptView.tsx b/app/src/pages/conversations/components/ProcessingTranscriptView.tsx index 5180c83033..d82ba2f1f7 100644 --- a/app/src/pages/conversations/components/ProcessingTranscriptView.tsx +++ b/app/src/pages/conversations/components/ProcessingTranscriptView.tsx @@ -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 = 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} ) : null} + {entry.status === 'error' && entry.failure ? ( + + ) : null} ); } +/** + * 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 ( + + + {t('conversations.toolFailure.whyLabel')}: {cause} + + + {t('conversations.toolFailure.nextLabel')}: {next} + + + ); +} + /** Compact terminal status glyph for the group's "Done" line. */ function StatusGlyph({ status }: { status: ToolTimelineEntryStatus }) { if (status === 'error') { diff --git a/app/src/providers/ChatRuntimeProvider.tsx b/app/src/providers/ChatRuntimeProvider.tsx index aadab9151a..0143c405d7 100644 --- a/app/src/providers/ChatRuntimeProvider.tsx +++ b/app/src/providers/ChatRuntimeProvider.tsx @@ -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; } diff --git a/app/src/services/chatService.ts b/app/src/services/chatService.ts index 94c0257a10..ddb1e55674 100644 --- a/app/src/services/chatService.ts +++ b/app/src/services/chatService.ts @@ -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; } diff --git a/app/src/store/chatRuntimeSlice.toolFailure.test.ts b/app/src/store/chatRuntimeSlice.toolFailure.test.ts new file mode 100644 index 0000000000..41640ce8cb --- /dev/null +++ b/app/src/store/chatRuntimeSlice.toolFailure.test.ts @@ -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(); + }); +}); diff --git a/app/src/store/chatRuntimeSlice.ts b/app/src/store/chatRuntimeSlice.ts index deedd6b641..f921108092 100644 --- a/app/src/store/chatRuntimeSlice.ts +++ b/app/src/store/chatRuntimeSlice.ts @@ -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; + 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), }; } diff --git a/app/src/types/turnState.ts b/app/src/types/turnState.ts index 544a803fda..8cb02c7e8b 100644 --- a/app/src/types/turnState.ts +++ b/app/src/types/turnState.ts @@ -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 {