feat(chat): add "hide agent thinking" toggle (#3994)

This commit is contained in:
Cyrus Gray
2026-06-23 21:47:17 +05:30
committed by GitHub
parent 3a2b38d0c3
commit 8cf289a904
21 changed files with 353 additions and 7 deletions
@@ -57,4 +57,16 @@ describe('<AppearancePanel /> 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);
});
});
@@ -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 = () => {
/>
}
/>
<SettingsRow
htmlFor="switch-hide-agent-insights"
label={t('settings.appearance.hideAgentInsights')}
description={t('settings.appearance.hideAgentInsightsDesc')}
control={
<SettingsSwitch
id="switch-hide-agent-insights"
checked={hideAgentInsights}
onCheckedChange={toggleHideAgentInsights}
aria-label={t('settings.appearance.hideAgentInsights')}
/>
}
/>
</SettingsSection>
{/* ── Display language (moved from the old settings home list) ── */}
+4
View File
@@ -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': 'نشط',
+4
View File
@@ -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': 'সক্রিয়',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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 lagent',
'settings.appearance.hideAgentInsightsDesc':
'Réduit la chronologie en direct des étapes de lagent dans le chat. Un lien clignotant « Traitement en cours » permet toujours douvrir 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',
+4
View File
@@ -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': 'एक्टिव',
+4
View File
@@ -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',
+4
View File
@@ -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 dellagente',
'settings.appearance.hideAgentInsightsDesc':
'Comprime la cronologia in tempo reale dei passaggi dellagente nella chat. Un link lampeggiante «Elaborazione» consente comunque di aprire lintero processo.',
'settings.appearance.assistantTextModeDesc':
"Mostra le risposte dell'assistente come testo senza cornice mantenendo i tuoi messaggi nei fumetti.",
'settings.mascot.active': 'Attivo',
+4
View File
@@ -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': '활성',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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': 'Активно',
+4
View File
@@ -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 角色',
+42 -6
View File
@@ -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 && (
<ToolTimelineBlock
entries={selectedThreadToolTimeline}
onViewSubagent={sub => 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 ? (
<button
type="button"
onClick={() => setShowProcessSource(true)}
data-testid="agent-processing-link"
className="flex items-center gap-1.5 px-1 py-1 text-[11px] font-medium text-primary-600 hover:underline dark:text-primary-300">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-primary-400 animate-pulse" />
<span>{t('conversations.agentTaskInsights.processing')} </span>
</button>
) : !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.
<button
type="button"
onClick={() => setShowProcessSource(true)}
data-testid="agent-process-source-fallback"
className="px-1 text-[11px] font-medium text-primary-600 hover:underline dark:text-primary-300">
{t('conversations.agentTaskInsights.viewProcessSource')}
</button>
) : null
) : (
<ToolTimelineBlock
entries={selectedThreadToolTimeline}
onViewSubagent={sub => 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
@@ -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<typeof buildStore> | 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<typeof buildStore> | 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<typeof buildStore> | 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)', () => {
+8 -1
View File
@@ -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);
+17
View File
@@ -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']);
+22
View File
@@ -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<boolean>) {
state.developerMode = action.payload;
},
setHideAgentInsights(state, action: PayloadAction<boolean>) {
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`.