From 8cf289a904686935f8d7a4cbcf696cc3df1f1d01 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:47:17 +0530 Subject: [PATCH] feat(chat): add "hide agent thinking" toggle (#3994) --- .../settings/panels/AppearancePanel.test.tsx | 12 ++ .../settings/panels/AppearancePanel.tsx | 18 ++ app/src/lib/i18n/ar.ts | 4 + app/src/lib/i18n/bn.ts | 4 + app/src/lib/i18n/de.ts | 4 + app/src/lib/i18n/en.ts | 4 + app/src/lib/i18n/es.ts | 4 + app/src/lib/i18n/fr.ts | 4 + app/src/lib/i18n/hi.ts | 4 + app/src/lib/i18n/id.ts | 4 + app/src/lib/i18n/it.ts | 4 + app/src/lib/i18n/ko.ts | 4 + app/src/lib/i18n/pl.ts | 4 + app/src/lib/i18n/pt.ts | 4 + app/src/lib/i18n/ru.ts | 4 + app/src/lib/i18n/zh-CN.ts | 4 + app/src/pages/Conversations.tsx | 48 ++++- .../__tests__/Conversations.render.test.tsx | 178 ++++++++++++++++++ app/src/store/index.ts | 9 +- app/src/store/themeSlice.test.ts | 17 ++ app/src/store/themeSlice.ts | 22 +++ 21 files changed, 353 insertions(+), 7 deletions(-) diff --git a/app/src/components/settings/panels/AppearancePanel.test.tsx b/app/src/components/settings/panels/AppearancePanel.test.tsx index d8eb80763..c67739090 100644 --- a/app/src/components/settings/panels/AppearancePanel.test.tsx +++ b/app/src/components/settings/panels/AppearancePanel.test.tsx @@ -57,4 +57,16 @@ describe(' font size', () => { expect(store.getState().theme.agentMessageViewMode).toBe('text'); }); + + it('toggles hide-agent-thinking on and off', () => { + const { getByRole, store } = renderPanel('medium'); + const toggle = getByRole('switch', { name: /settings\.appearance\.hideAgentInsights/ }); + + expect(toggle).toHaveAttribute('aria-checked', 'false'); + fireEvent.click(toggle); + expect(store.getState().theme.hideAgentInsights).toBe(true); + + fireEvent.click(toggle); + expect(store.getState().theme.hideAgentInsights).toBe(false); + }); }); diff --git a/app/src/components/settings/panels/AppearancePanel.tsx b/app/src/components/settings/panels/AppearancePanel.tsx index 73f7ca9d9..53cd4f065 100644 --- a/app/src/components/settings/panels/AppearancePanel.tsx +++ b/app/src/components/settings/panels/AppearancePanel.tsx @@ -7,6 +7,7 @@ import { type FontSize, setAgentMessageViewMode, setFontSize, + setHideAgentInsights, setTabBarLabels, setThemeMode, type TabBarLabels, @@ -76,6 +77,7 @@ const AppearancePanel = () => { const agentMessageViewMode = useAppSelector( state => state.theme.agentMessageViewMode ?? 'bubbles' ); + const hideAgentInsights = useAppSelector(state => state.theme.hideAgentInsights ?? false); const labelsAlwaysVisible = tabBarLabels === 'always'; const assistantTextModeEnabled = agentMessageViewMode === 'text'; const toggleTabBarLabels = () => { @@ -86,6 +88,9 @@ const AppearancePanel = () => { const next: AgentMessageViewMode = assistantTextModeEnabled ? 'bubbles' : 'text'; dispatch(setAgentMessageViewMode(next)); }; + const toggleHideAgentInsights = () => { + dispatch(setHideAgentInsights(!hideAgentInsights)); + }; // Build at render time so the labels follow the active locale; `t()` itself // memoises on locale change, so this stays stable across re-renders within a @@ -309,6 +314,19 @@ const AppearancePanel = () => { /> } /> + + } + /> {/* ── Display language (moved from the old settings home list) ── */} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 393d202b8..3162645d0 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -3003,6 +3003,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'المصادر', 'conversations.agentTaskInsights.noSteps': 'لم يتم تسجيل أي خطوات', 'conversations.agentTaskInsights.viewProcessSource': 'عرض مصدر عملية الوكيل الكامل', + 'conversations.agentTaskInsights.processing': 'قيد المعالجة', 'daemon.serviceBlockingGate.body': 'المحتوى', 'daemon.serviceBlockingGate.downloadHint': 'تلميح التنزيل', 'daemon.serviceBlockingGate.downloadLatest': 'تنزيل أحدث إصدار', @@ -4509,6 +4510,9 @@ const messages: TranslationMap = { 'عند إيقاف التشغيل، تظهر التسميات فقط عند التمرير أو لعلامة التبويب النشطة.', 'settings.appearance.chatHeading': 'الدردشة', 'settings.appearance.assistantTextMode': 'ردود المساعد كنص', + 'settings.appearance.hideAgentInsights': 'إخفاء تفكير الوكيل', + 'settings.appearance.hideAgentInsightsDesc': + 'طيّ الجدول الزمني المباشر لخطوات الوكيل في المحادثة. سيظل رابط "قيد المعالجة" الوامض يتيح لك فتح العملية الكاملة.', 'settings.appearance.assistantTextModeDesc': 'اعرض ردود المساعد كنص بلا إطار مع إبقاء رسائلك داخل فقاعات.', 'settings.mascot.active': 'نشط', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index b14158b8f..cd954a653 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -3067,6 +3067,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'উৎসসমূহ', 'conversations.agentTaskInsights.noSteps': 'কোনো ধাপ রেকর্ড করা হয়নি', 'conversations.agentTaskInsights.viewProcessSource': 'সম্পূর্ণ এজেন্ট প্রক্রিয়ার উৎস দেখুন', + 'conversations.agentTaskInsights.processing': 'প্রসেসিং', 'daemon.serviceBlockingGate.body': 'বডি', 'daemon.serviceBlockingGate.downloadHint': 'ডাউনলোড হিন্ট', 'daemon.serviceBlockingGate.downloadLatest': 'সর্বশেষ সংস্করণ ডাউনলোড করুন', @@ -4602,6 +4603,9 @@ const messages: TranslationMap = { 'বন্ধ থাকা অবস্থায়, লেবেলগুলি শুধুমাত্র হোভারে বা সক্রিয় ট্যাবের জন্য প্রদর্শিত হয়৷', 'settings.appearance.chatHeading': 'চ্যাট', 'settings.appearance.assistantTextMode': 'অ্যাসিস্ট্যান্টের উত্তর টেক্সট হিসেবে', + 'settings.appearance.hideAgentInsights': 'এজেন্টের চিন্তা লুকান', + 'settings.appearance.hideAgentInsightsDesc': + 'চ্যাটে এজেন্টের ধাপে ধাপে লাইভ টাইমলাইন সংকুচিত করুন। একটি ব্লিঙ্কিং "প্রসেসিং" লিঙ্ক এখনও আপনাকে সম্পূর্ণ প্রক্রিয়া খুলতে দেয়।', 'settings.appearance.assistantTextModeDesc': 'আপনার বার্তাগুলি বাবলে রেখে অ্যাসিস্ট্যান্টের উত্তর ফ্রেমহীন টেক্সট হিসেবে দেখান।', 'settings.mascot.active': 'সক্রিয়', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 885db709d..1afa1085b 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -3141,6 +3141,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.noSteps': 'Keine Schritte aufgezeichnet', 'conversations.agentTaskInsights.viewProcessSource': 'Vollständige Agentenprozess-Quelle anzeigen', + 'conversations.agentTaskInsights.processing': 'Wird verarbeitet', 'daemon.serviceBlockingGate.body': 'Körper', 'daemon.serviceBlockingGate.downloadHint': 'Hinweis herunterladen', 'daemon.serviceBlockingGate.downloadLatest': 'Lade die neueste Version herunter', @@ -4721,6 +4722,9 @@ const messages: TranslationMap = { 'Wenn diese Option deaktiviert ist, werden Beschriftungen nur beim Hover oder für die aktive Registerkarte angezeigt.', 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': 'Assistentenantworten als Text', + 'settings.appearance.hideAgentInsights': 'Agent-Denkprozess ausblenden', + 'settings.appearance.hideAgentInsightsDesc': + 'Blendet die schrittweise Live-Zeitleiste des Agenten im Chat aus. Über einen blinkenden „Wird verarbeitet“-Link lässt sich der vollständige Ablauf weiterhin öffnen.', 'settings.appearance.assistantTextModeDesc': 'Zeigt Assistentenantworten als ungerahmten Text an und lässt deine Nachrichten in Blasen.', 'settings.mascot.active': 'Aktiv', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 01ae6dd38..df9fc54e7 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -3614,6 +3614,7 @@ const en: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'Sources', 'conversations.agentTaskInsights.noSteps': 'No steps recorded', 'conversations.agentTaskInsights.viewProcessSource': 'View full agent process Source', + 'conversations.agentTaskInsights.processing': 'Processing', 'daemon.serviceBlockingGate.body': 'Retrying in the background. This usually resolves in a few seconds.', 'daemon.serviceBlockingGate.downloadHint': @@ -5204,6 +5205,9 @@ const en: TranslationMap = { 'settings.appearance.assistantTextMode': 'Plain assistant responses', 'settings.appearance.assistantTextModeDesc': 'Render assistant replies as unframed text while keeping your messages in bubbles.', + 'settings.appearance.hideAgentInsights': 'Hide agent thinking', + 'settings.appearance.hideAgentInsightsDesc': + 'Collapse the live step-by-step agent timeline in chat. A blinking “Processing” link still lets you open the full run.', 'settings.mascot.active': 'Active', 'settings.mascot.characterDesc': 'Choose your OpenHuman character.', 'settings.mascot.characterHeading': 'Character', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index ddff5829c..09c9ca174 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -3119,6 +3119,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.noSteps': 'No hay pasos registrados', 'conversations.agentTaskInsights.viewProcessSource': 'Ver la fuente completa del proceso del agente', + 'conversations.agentTaskInsights.processing': 'Procesando', 'daemon.serviceBlockingGate.body': 'Cuerpo', 'daemon.serviceBlockingGate.downloadHint': 'Sugerencia de descarga', 'daemon.serviceBlockingGate.downloadLatest': 'Descargar la última versión', @@ -4690,6 +4691,9 @@ const messages: TranslationMap = { 'Cuando está desactivado, las etiquetas solo aparecen al pasar el mouse o para la pestaña activa.', 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': 'Respuestas del asistente en texto', + 'settings.appearance.hideAgentInsights': 'Ocultar el razonamiento del agente', + 'settings.appearance.hideAgentInsightsDesc': + 'Contrae la cronología paso a paso del agente en el chat. Un enlace «Procesando» parpadeante te permite abrir el proceso completo.', 'settings.appearance.assistantTextModeDesc': 'Muestra las respuestas del asistente como texto sin marco y mantiene tus mensajes en burbujas.', 'settings.mascot.active': 'Activo', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 0d416bb6a..82967e49f 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -3134,6 +3134,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.noSteps': 'Aucune étape enregistrée', 'conversations.agentTaskInsights.viewProcessSource': "Voir la source complète du processus de l'agent", + 'conversations.agentTaskInsights.processing': 'Traitement en cours', 'daemon.serviceBlockingGate.body': 'Corps', 'daemon.serviceBlockingGate.downloadHint': 'Indice de téléchargement', 'daemon.serviceBlockingGate.downloadLatest': 'Télécharger la dernière version', @@ -4707,6 +4708,9 @@ const messages: TranslationMap = { "Lorsqu'elle est désactivée, les étiquettes n'apparaissent qu'au survol ou pour l'onglet actif.", 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': "Réponses de l'assistant en texte", + 'settings.appearance.hideAgentInsights': 'Masquer le raisonnement de l’agent', + 'settings.appearance.hideAgentInsightsDesc': + 'Réduit la chronologie en direct des étapes de l’agent dans le chat. Un lien clignotant « Traitement en cours » permet toujours d’ouvrir le déroulé complet.', 'settings.appearance.assistantTextModeDesc': "Affiche les réponses de l'assistant en texte sans cadre tout en gardant vos messages en bulles.", 'settings.mascot.active': 'Actif', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 6702e2589..f986a2ed3 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -3068,6 +3068,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'स्रोत', 'conversations.agentTaskInsights.noSteps': 'कोई चरण दर्ज नहीं किया गया', 'conversations.agentTaskInsights.viewProcessSource': 'पूर्ण एजेंट प्रक्रिया स्रोत देखें', + 'conversations.agentTaskInsights.processing': 'प्रोसेसिंग', 'daemon.serviceBlockingGate.body': 'विवरण', 'daemon.serviceBlockingGate.downloadHint': 'डाउनलोड संकेत', 'daemon.serviceBlockingGate.downloadLatest': 'नवीनतम संस्करण डाउनलोड करें', @@ -4606,6 +4607,9 @@ const messages: TranslationMap = { 'बंद होने पर, लेबल केवल होवर पर या सक्रिय टैब के लिए दिखाई देते हैं।', 'settings.appearance.chatHeading': 'चैट', 'settings.appearance.assistantTextMode': 'असिस्टेंट जवाब टेक्स्ट में', + 'settings.appearance.hideAgentInsights': 'एजेंट की सोच छिपाएँ', + 'settings.appearance.hideAgentInsightsDesc': + 'चैट में एजेंट की चरण-दर-चरण लाइव टाइमलाइन को छिपाएँ। एक ब्लिंक करता "प्रोसेसिंग" लिंक फिर भी आपको पूरी प्रक्रिया खोलने देता है।', 'settings.appearance.assistantTextModeDesc': 'असिस्टेंट के जवाबों को बिना फ्रेम वाले टेक्स्ट के रूप में दिखाएं और आपके संदेश बबल में रखें।', 'settings.mascot.active': 'एक्टिव', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 0a0ec5946..2bef589bf 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -3071,6 +3071,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'Sumber', 'conversations.agentTaskInsights.noSteps': 'Tidak ada langkah yang tercatat', 'conversations.agentTaskInsights.viewProcessSource': 'Lihat sumber proses agen lengkap', + 'conversations.agentTaskInsights.processing': 'Memproses', 'daemon.serviceBlockingGate.body': 'Isi', 'daemon.serviceBlockingGate.downloadHint': 'Petunjuk unduhan', 'daemon.serviceBlockingGate.downloadLatest': 'Unduh Versi Terbaru', @@ -4615,6 +4616,9 @@ const messages: TranslationMap = { 'Saat nonaktif, label hanya muncul saat diarahkan atau untuk tab yang aktif.', 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': 'Respons asisten sebagai teks', + 'settings.appearance.hideAgentInsights': 'Sembunyikan proses berpikir agen', + 'settings.appearance.hideAgentInsightsDesc': + 'Ciutkan lini masa langkah demi langkah agen secara langsung di obrolan. Tautan "Memproses" yang berkedip tetap memungkinkan Anda membuka proses lengkapnya.', 'settings.appearance.assistantTextModeDesc': 'Tampilkan balasan asisten sebagai teks tanpa bingkai, sementara pesan Anda tetap dalam gelembung.', 'settings.mascot.active': 'Aktif', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 4bda9cf33..985b79647 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -3113,6 +3113,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.noSteps': 'Nessun passaggio registrato', 'conversations.agentTaskInsights.viewProcessSource': "Visualizza l'origine completa del processo dell'agente", + 'conversations.agentTaskInsights.processing': 'Elaborazione', 'daemon.serviceBlockingGate.body': 'Corpo', 'daemon.serviceBlockingGate.downloadHint': 'Suggerimento di download', 'daemon.serviceBlockingGate.downloadLatest': "Scarica l'ultima versione", @@ -4678,6 +4679,9 @@ const messages: TranslationMap = { 'Quando disattivata, le etichette vengono visualizzate solo al passaggio del mouse o per la scheda attiva.', 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': "Risposte dell'assistente in testo", + 'settings.appearance.hideAgentInsights': 'Nascondi il ragionamento dell’agente', + 'settings.appearance.hideAgentInsightsDesc': + 'Comprime la cronologia in tempo reale dei passaggi dell’agente nella chat. Un link lampeggiante «Elaborazione» consente comunque di aprire l’intero processo.', 'settings.appearance.assistantTextModeDesc': "Mostra le risposte dell'assistente come testo senza cornice mantenendo i tuoi messaggi nei fumetti.", 'settings.mascot.active': 'Attivo', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index a53081415..aa9eb989d 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -3043,6 +3043,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': '소스', 'conversations.agentTaskInsights.noSteps': '기록된 단계 없음', 'conversations.agentTaskInsights.viewProcessSource': '전체 에이전트 프로세스 소스 보기', + 'conversations.agentTaskInsights.processing': '처리 중', 'daemon.serviceBlockingGate.body': '본문', 'daemon.serviceBlockingGate.downloadHint': '다운로드 안내', 'daemon.serviceBlockingGate.downloadLatest': '최신 버전 다운로드', @@ -4558,6 +4559,9 @@ const messages: TranslationMap = { '끄면 레이블은 마우스를 가져가거나 활성 탭에 대해서만 표시됩니다.', 'settings.appearance.chatHeading': '채팅', 'settings.appearance.assistantTextMode': '어시스턴트 답변을 텍스트로 표시', + 'settings.appearance.hideAgentInsights': '에이전트 사고 숨기기', + 'settings.appearance.hideAgentInsightsDesc': + '채팅에서 에이전트의 단계별 실시간 타임라인을 접습니다. 깜박이는 "처리 중" 링크로 전체 과정을 열 수 있습니다.', 'settings.appearance.assistantTextModeDesc': '사용자 메시지는 말풍선으로 유지하고 어시스턴트 답변은 프레임 없는 텍스트로 표시합니다.', 'settings.mascot.active': '활성', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 1c553c67a..b4b922d61 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -3099,6 +3099,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'Źródła', 'conversations.agentTaskInsights.noSteps': 'Brak zarejestrowanych kroków', 'conversations.agentTaskInsights.viewProcessSource': 'Zobacz pełne źródło procesu agenta', + 'conversations.agentTaskInsights.processing': 'Przetwarzanie', 'daemon.serviceBlockingGate.body': 'Rdzeń OpenHuman nie odpowiada. Spróbuj ponownie lub pobierz najnowszą wersję aplikacji.', 'daemon.serviceBlockingGate.downloadHint': @@ -4672,6 +4673,9 @@ const messages: TranslationMap = { 'Wyłączone — etykiety pojawiają się tylko po najechaniu lub dla aktywnej zakładki.', 'settings.appearance.chatHeading': 'Czat', 'settings.appearance.assistantTextMode': 'Odpowiedzi asystenta jako tekst', + 'settings.appearance.hideAgentInsights': 'Ukryj myślenie agenta', + 'settings.appearance.hideAgentInsightsDesc': + 'Zwija oś czasu z krokami agenta na żywo w czacie. Migający link „Przetwarzanie” nadal pozwala otworzyć pełny przebieg.', 'settings.appearance.assistantTextModeDesc': 'Wyświetla odpowiedzi asystenta jako tekst bez ramki, a Twoje wiadomości pozostawia w dymkach.', 'settings.mascot.active': 'Aktywny', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index e9efe16a2..e4c4b801a 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -3117,6 +3117,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'Fontes', 'conversations.agentTaskInsights.noSteps': 'Nenhuma etapa registrada', 'conversations.agentTaskInsights.viewProcessSource': 'Ver a fonte completa do processo do agente', + 'conversations.agentTaskInsights.processing': 'Processando', 'daemon.serviceBlockingGate.body': 'Corpo', 'daemon.serviceBlockingGate.downloadHint': 'Dica de download', 'daemon.serviceBlockingGate.downloadLatest': 'Baixar Versão Mais Recente', @@ -4682,6 +4683,9 @@ const messages: TranslationMap = { 'Quando desativado, os rótulos só aparecem ao passar o mouse ou para a guia ativa.', 'settings.appearance.chatHeading': 'Chat', 'settings.appearance.assistantTextMode': 'Respostas do assistente em texto', + 'settings.appearance.hideAgentInsights': 'Ocultar o raciocínio do agente', + 'settings.appearance.hideAgentInsightsDesc': + 'Recolhe a linha do tempo passo a passo do agente no chat. Um link “Processando” piscando ainda permite abrir o processo completo.', 'settings.appearance.assistantTextModeDesc': 'Renderiza as respostas do assistente como texto sem moldura e mantém suas mensagens em balões.', 'settings.mascot.active': 'Ativo', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 4f966ae2c..223a55822 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -3090,6 +3090,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': 'Источники', 'conversations.agentTaskInsights.noSteps': 'Шаги не записаны', 'conversations.agentTaskInsights.viewProcessSource': 'Показать полный источник процесса агента', + 'conversations.agentTaskInsights.processing': 'Обработка', 'daemon.serviceBlockingGate.body': 'Текст', 'daemon.serviceBlockingGate.downloadHint': 'Подсказка по загрузке', 'daemon.serviceBlockingGate.downloadLatest': 'Скачать последнюю версию', @@ -4645,6 +4646,9 @@ const messages: TranslationMap = { 'Если этот параметр отключен, метки отображаются только при наведении курсора мыши или на активной вкладке.', 'settings.appearance.chatHeading': 'Чат', 'settings.appearance.assistantTextMode': 'Ответы ассистента текстом', + 'settings.appearance.hideAgentInsights': 'Скрыть размышления агента', + 'settings.appearance.hideAgentInsightsDesc': + 'Сворачивает пошаговую ленту действий агента в чате. Мигающая ссылка «Обработка» по-прежнему позволяет открыть весь процесс.', 'settings.appearance.assistantTextModeDesc': 'Показывает ответы ассистента как текст без рамки, оставляя ваши сообщения в пузырьках.', 'settings.mascot.active': 'Активно', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 8c4cb48ad..b4c964dcd 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -2920,6 +2920,7 @@ const messages: TranslationMap = { 'conversations.agentTaskInsights.sourcesHeading': '来源', 'conversations.agentTaskInsights.noSteps': '未记录任何步骤', 'conversations.agentTaskInsights.viewProcessSource': '查看完整的智能体处理来源', + 'conversations.agentTaskInsights.processing': '处理中', 'daemon.serviceBlockingGate.body': '核心服务不可用,请等待或下载最新版本。', 'daemon.serviceBlockingGate.downloadHint': '下载最新版本', 'daemon.serviceBlockingGate.downloadLatest': '下载最新版本', @@ -4379,6 +4380,9 @@ const messages: TranslationMap = { 'settings.appearance.tabBarAlwaysShowLabelsDesc': '关闭时,标签仅出现在悬停时或活动选项卡上。', 'settings.appearance.chatHeading': '聊天', 'settings.appearance.assistantTextMode': '助手回复以文本显示', + 'settings.appearance.hideAgentInsights': '隐藏智能体思考过程', + 'settings.appearance.hideAgentInsightsDesc': + '折叠聊天中智能体逐步执行的实时时间线。闪烁的“处理中”链接仍可让你打开完整过程。', 'settings.appearance.assistantTextModeDesc': '将助手回复渲染为无边框文本,同时保留你的消息气泡。', 'settings.mascot.active': '活跃', 'settings.mascot.characterDesc': '选择你的 OpenHuman 角色', diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index e29992553..20e30d936 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -304,6 +304,11 @@ const Conversations = ({ const agentMessageViewMode = useAppSelector( state => state.theme?.agentMessageViewMode ?? 'bubbles' ); + // When ON, the verbose per-agent "Agentic task insights" timeline is hidden + // from chat; a compact blinking "Processing" link (and the existing message + // bubble loading) stand in for it, with the full run one click away in the + // Agent Process Source side panel. See themeSlice.hideAgentInsights. + const hideAgentInsights = useAppSelector(state => state.theme?.hideAgentInsights ?? false); const inferenceTurnLifecycleByThread = useAppSelector( state => state.chatRuntime.inferenceTurnLifecycleByThread ); @@ -2233,12 +2238,43 @@ const Conversations = ({ messages the turn produced — both for the settled/inline case (shouldRenderTimelineBeforeLatestAgentMessage) and the live in-flight fallback. */} - {selectedThreadToolTimeline.length > 0 && ( - setOpenSubagentTaskId(sub.taskId)} - /> - )} + {selectedThreadToolTimeline.length > 0 && + (hideAgentInsights ? ( + // "Hide agent thinking" is ON: suppress the verbose step rows. + // While the turn is still in flight, surface a single compact + // blinking "Processing" link that opens the full run in the + // Agent Process Source side panel. Once settled, the + // "View full agent process Source" button below takes over. + isSending ? ( + + ) : !shouldRenderTimelineBeforeLatestAgentMessage ? ( + // Settled, but the hoisted "View full agent process Source" + // opener below won't render because no agent message exists + // for this turn (e.g. a cancelled first turn — onError skips + // the agent message for `error_type === 'cancelled'`). Without + // this fallback the recorded steps would be unreachable while + // hidden, so surface our own opener whenever entries remain. + + ) : null + ) : ( + setOpenSubagentTaskId(sub.taskId)} + /> + ))} {/* "View full agent process" — only in the settled/inline state (turn finished, an agent message exists). Hoisted out of the per-message map alongside the panel above so it renders once diff --git a/app/src/pages/__tests__/Conversations.render.test.tsx b/app/src/pages/__tests__/Conversations.render.test.tsx index 1b447656a..eb49556bf 100644 --- a/app/src/pages/__tests__/Conversations.render.test.tsx +++ b/app/src/pages/__tests__/Conversations.render.test.tsx @@ -19,6 +19,7 @@ import { chatSend } from '../../services/chatService'; import { CoreRpcError } from '../../services/coreRpcClient'; import agentProfileReducer from '../../store/agentProfileSlice'; import chatRuntimeReducer, { + beginInferenceTurn, setInferenceStatusForThread, setTaskBoardForThread, setToolTimelineForThread, @@ -1778,6 +1779,183 @@ describe('Conversations — agent task insights panel anchoring (#3717 Bug 2)', fireEvent.click(screen.getByTestId('subagent-view-processing')); }); }); + + it('hides the verbose timeline when "hide agent thinking" is on, but still opens the source panel', async () => { + const thread = makeThread({ id: 'hide-insights-thread', title: 'Hide insights' }); + const messages: ThreadMessage[] = [ + { + id: 'm-user', + sender: 'user', + type: 'text', + content: 'How many posts?', + extraMetadata: {}, + createdAt: '2026-01-01T00:00:00.000Z', + }, + { + id: 'm-agent', + sender: 'agent', + type: 'text', + content: 'Zero posts went up.', + extraMetadata: {}, + createdAt: '2026-01-01T00:01:00.000Z', + }, + ]; + mockGetThreads.mockResolvedValue({ threads: [thread], count: 1 }); + mockGetThreadMessages.mockResolvedValue({ messages, count: messages.length }); + + let store: ReturnType | undefined; + await act(async () => { + store = await renderConversations({ + thread: { + ...selectedThreadState(thread), + messagesByThreadId: { [thread.id]: messages }, + messages, + }, + socket: socketState('connected'), + theme: { + mode: 'system', + tabBarLabels: 'hover', + fontSize: 'medium', + hideAgentInsights: true, + }, + }); + }); + + await screen.findByText('Zero posts went up.'); + await act(async () => { + store!.dispatch( + setToolTimelineForThread({ + threadId: thread.id, + entries: [{ id: 'tl-1', name: 'web_fetch', round: 1, status: 'success' }], + }) + ); + }); + + // Settled turn + preference ON: the verbose inline timeline is suppressed… + expect(screen.queryByTestId('agent-task-insights')).toBeNull(); + // …but the "View full agent process Source" affordance still works and the + // full run is one click away in the side panel (which renders the timeline). + await act(async () => { + fireEvent.click(screen.getByTestId('view-process-source')); + }); + expect(await screen.findByTestId('agent-task-insights')).toBeInTheDocument(); + }); + + it('shows a blinking "Processing" link instead of the timeline while in flight', async () => { + const thread = makeThread({ id: 'processing-thread', title: 'Processing' }); + const messages: ThreadMessage[] = [ + { + id: 'm-user', + sender: 'user', + type: 'text', + content: 'Go.', + extraMetadata: {}, + createdAt: '2026-01-01T00:00:00.000Z', + }, + ]; + mockGetThreads.mockResolvedValue({ threads: [thread], count: 1 }); + mockGetThreadMessages.mockResolvedValue({ messages, count: messages.length }); + + let store: ReturnType | undefined; + await act(async () => { + store = await renderConversations({ + thread: { + ...selectedThreadState(thread), + messagesByThreadId: { [thread.id]: messages }, + messages, + }, + socket: socketState('connected'), + theme: { + mode: 'system', + tabBarLabels: 'hover', + fontSize: 'medium', + hideAgentInsights: true, + }, + }); + }); + + await screen.findByText('Go.'); + // Drive the thread into an in-flight turn so `isSending` is true, then seed + // a running timeline that the preference should keep hidden behind the link. + await act(async () => { + store!.dispatch(beginInferenceTurn({ threadId: thread.id })); + store!.dispatch( + setToolTimelineForThread({ + threadId: thread.id, + entries: [{ id: 'tl-1', name: 'web_fetch', round: 1, status: 'running' }], + }) + ); + }); + + const link = await screen.findByTestId('agent-processing-link'); + expect(link).toBeInTheDocument(); + expect(screen.queryByTestId('agent-task-insights')).toBeNull(); + + // Clicking the compact link opens the full run in the source panel. + await act(async () => { + fireEvent.click(link); + }); + expect(await screen.findByTestId('agent-task-insights')).toBeInTheDocument(); + }); + + it('keeps a settled source opener when hidden and no agent message exists (cancelled first turn)', async () => { + // A cancelled first turn records timeline steps but never persists an agent + // message, so the hoisted "View full agent process Source" button does not + // render. With the timeline hidden, the fallback opener must keep those + // steps reachable. + const thread = makeThread({ id: 'cancelled-first-turn', title: 'Cancelled' }); + const messages: ThreadMessage[] = [ + { + id: 'm-user', + sender: 'user', + type: 'text', + content: 'Start then stop.', + extraMetadata: {}, + createdAt: '2026-01-01T00:00:00.000Z', + }, + ]; + mockGetThreads.mockResolvedValue({ threads: [thread], count: 1 }); + mockGetThreadMessages.mockResolvedValue({ messages, count: messages.length }); + + let store: ReturnType | undefined; + await act(async () => { + store = await renderConversations({ + thread: { + ...selectedThreadState(thread), + messagesByThreadId: { [thread.id]: messages }, + messages, + }, + socket: socketState('connected'), + theme: { + mode: 'system', + tabBarLabels: 'hover', + fontSize: 'medium', + hideAgentInsights: true, + }, + }); + }); + + await screen.findByText('Start then stop.'); + // Settled (no in-flight turn) timeline with steps but no agent message. + await act(async () => { + store!.dispatch( + setToolTimelineForThread({ + threadId: thread.id, + entries: [{ id: 'tl-1', name: 'web_fetch', round: 1, status: 'cancelled' }], + }) + ); + }); + + // No verbose timeline, and the hoisted opener is absent (no agent message)… + expect(screen.queryByTestId('agent-task-insights')).toBeNull(); + expect(screen.queryByTestId('view-process-source')).toBeNull(); + // …so the fallback opener carries access to the recorded steps. + const fallback = await screen.findByTestId('agent-process-source-fallback'); + await act(async () => { + fireEvent.click(fallback); + }); + expect(await screen.findByTestId('agent-task-insights')).toBeInTheDocument(); + }); }); describe('Conversations — open-session resume (View work)', () => { diff --git a/app/src/store/index.ts b/app/src/store/index.ts index 3427dbb7f..fbaa59dc9 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -96,7 +96,14 @@ const persistedLocaleReducer = persistReducer(localePersistConfig, localeReducer const themePersistConfig = { key: 'theme', storage: localStorageAdapter, - whitelist: ['mode', 'tabBarLabels', 'fontSize', 'agentMessageViewMode', 'developerMode'], + whitelist: [ + 'mode', + 'tabBarLabels', + 'fontSize', + 'agentMessageViewMode', + 'developerMode', + 'hideAgentInsights', + ], }; const persistedThemeReducer = persistReducer(themePersistConfig, themeReducer); diff --git a/app/src/store/themeSlice.test.ts b/app/src/store/themeSlice.test.ts index 1965423f0..90e7426df 100644 --- a/app/src/store/themeSlice.test.ts +++ b/app/src/store/themeSlice.test.ts @@ -3,8 +3,10 @@ import { describe, expect, it } from 'vitest'; import themeReducer, { FONT_SIZE_PX, type FontSize, + selectHideAgentInsights, setAgentMessageViewMode, setFontSize, + setHideAgentInsights, setTabBarLabels, setThemeMode, } from './themeSlice'; @@ -39,6 +41,7 @@ describe('themeSlice', () => { fontSize: 'xlarge', agentMessageViewMode: 'text', developerMode: false, + hideAgentInsights: false, }); }); @@ -48,6 +51,20 @@ describe('themeSlice', () => { expect(state.agentMessageViewMode).toBe('text'); }); + it('defaults hideAgentInsights to false and toggles it', () => { + let state = themeReducer(undefined, { type: '@@INIT' }); + expect(state.hideAgentInsights).toBe(false); + expect(selectHideAgentInsights({ theme: state })).toBe(false); + + state = themeReducer(state, setHideAgentInsights(true)); + expect(state.hideAgentInsights).toBe(true); + expect(selectHideAgentInsights({ theme: state })).toBe(true); + }); + + it('falls back to false when hideAgentInsights is absent from persisted state', () => { + expect(selectHideAgentInsights({ theme: {} as never })).toBe(false); + }); + it('maps every font size to a concrete px value', () => { const sizes: FontSize[] = ['small', 'medium', 'large', 'xlarge']; expect(sizes.map(size => FONT_SIZE_PX[size])).toEqual(['14px', '16px', '18px', '20px']); diff --git a/app/src/store/themeSlice.ts b/app/src/store/themeSlice.ts index 3e1455025..f96f8ccad 100644 --- a/app/src/store/themeSlice.ts +++ b/app/src/store/themeSlice.ts @@ -36,6 +36,15 @@ interface ThemeState { * is authoritative and is never relaxed by this toggle. */ developerMode: boolean; + /** + * Hide the live "Agentic task insights" step-by-step timeline in chat + * (default OFF). When true, the verbose per-agent step rows are collapsed + * away: the chat shows only the existing message-bubble loading plus a + * compact blinking "Processing" link while a turn is in flight. The full + * timeline is still one click away via that link / the "View full agent + * process Source" affordance, which open the existing side panel. + */ + hideAgentInsights: boolean; } const initialState: ThemeState = { @@ -44,6 +53,7 @@ const initialState: ThemeState = { fontSize: 'medium', agentMessageViewMode: 'text', developerMode: false, + hideAgentInsights: false, }; const themeSlice = createSlice({ @@ -65,6 +75,9 @@ const themeSlice = createSlice({ setDeveloperMode(state, action: PayloadAction) { state.developerMode = action.payload; }, + setHideAgentInsights(state, action: PayloadAction) { + state.hideAgentInsights = action.payload; + }, }, }); @@ -74,9 +87,18 @@ export const { setFontSize, setAgentMessageViewMode, setDeveloperMode, + setHideAgentInsights, } = themeSlice.actions; export default themeSlice.reducer; +/** + * Selector for the persisted `hideAgentInsights` preference. Falls back to + * `false` so existing persisted state (written before this field existed) + * keeps the verbose timeline visible until the user opts out. + */ +export const selectHideAgentInsights = (state: { theme: ThemeState }): boolean => + state.theme.hideAgentInsights ?? false; + /** * Selector for the persisted `developerMode` preference. * Use {@link useDeveloperMode} in components — it combines this with `IS_DEV`.