(function () {
    window.AgentFrontendModules = window.AgentFrontendModules || {};

    window.AgentFrontendModules.createChatRenderingModule = function createChatRenderingModule(deps) {
        let forceStickBottomUntil = 0;
        let chatObserverBound = false;

        function ensureChatThreadVisibleState() {
            const chatThread = deps.refs.chatThread;
            const chatThreadEmpty = deps.refs.chatThreadEmpty;
            if (!chatThread || !chatThreadEmpty) return;
            const hasVisibleContent = Array.from(chatThread.children).some((node) => {
                if (!(node instanceof HTMLElement)) return false;
                if (node.id === 'chat-thread-empty') return false;
                if (node.style.display === 'none') return false;
                return true;
            });
            chatThread.classList.toggle('has-messages', hasVisibleContent);
            if (hasVisibleContent) {
                chatThreadEmpty.style.display = 'none';
                chatThreadEmpty.style.height = '0';
                chatThreadEmpty.style.minHeight = '0';
                chatThreadEmpty.style.padding = '0';
                chatThreadEmpty.style.margin = '0';
                chatThreadEmpty.style.overflow = 'hidden';
                chatThreadEmpty.hidden = true;
            } else {
                chatThreadEmpty.hidden = false;
                chatThreadEmpty.style.display = '';
                chatThreadEmpty.style.height = '';
                chatThreadEmpty.style.minHeight = '';
                chatThreadEmpty.style.padding = '';
                chatThreadEmpty.style.margin = '';
                chatThreadEmpty.style.overflow = '';
            }
        }

        function compactChatThreadLayout() {
            const chatThread = deps.refs.chatThread;
            const chatThreadEmpty = deps.refs.chatThreadEmpty;
            if (!chatThread) return;
            const collapseNode = (node) => {
                node.dataset.compactHidden = '1';
                node.style.display = 'none';
                node.style.height = '0';
                node.style.margin = '0';
                node.style.padding = '0';
            };
            const restoreNode = (node) => {
                if (node.dataset.compactHidden === '1') {
                    delete node.dataset.compactHidden;
                    node.style.display = '';
                    node.style.height = '';
                    node.style.margin = '';
                    node.style.padding = '';
                }
            };
            const hasBubbles = !!chatThread.querySelector('.chat-bubble');
            if (hasBubbles && chatThreadEmpty) {
                chatThreadEmpty.style.display = 'none';
                chatThreadEmpty.style.flex = '0 0 auto';
                chatThreadEmpty.style.minHeight = '0';
                chatThreadEmpty.style.padding = '0';
                chatThreadEmpty.style.margin = '0';
            }
            Array.from(chatThread.children).forEach((node) => {
                if (!(node instanceof HTMLElement)) return;
                if (node.id === 'chat-thread-empty') return;
                if (node.classList.contains('thinking-bubble')) {
                    const block = node.querySelector('.thinking-block');
                    const body = block?.querySelector('.thinking-body');
                    const hasDots = !!node.querySelector('.thinking-dots');
                    const blockVisible = !!block && getComputedStyle(block).display !== 'none';
                    const blockText = String(body?.textContent || '').replace(/\s+/g, '').trim();
                    if (!hasDots && (!block || !blockVisible || !blockText)) {
                        collapseNode(node);
                        return;
                    }
                    restoreNode(node);
                }
                node.style.flex = '0 0 auto';
                node.style.minHeight = '0';
                const meaningfulVisible = !!Array.from(node.querySelectorAll('img,table,pre,code,svg,.thinking-dots,.thinking-block,.thinking-body,.agent-activity-content,.report-body-inline,.chat-bubble-text,.workflow-status-row'))
                    .find((el) => {
                        if (!(el instanceof HTMLElement)) return false;
                        const cs = getComputedStyle(el);
                        if (cs.display === 'none' || cs.visibility === 'hidden') return false;
                        if (el.matches('img,table,pre,code,svg,.thinking-dots,.thinking-block,.thinking-body,.agent-activity-content,.report-body-inline,.workflow-status-row')) return true;
                        return !!String(el.textContent || '').replace(/\s+/g, '').trim();
                    });
                const txt = String(node.textContent || '').replace(/\s+/g, '').trim();
                if (!txt && !meaningfulVisible) {
                    collapseNode(node);
                    return;
                }
                restoreNode(node);
                if (node.classList.contains('app') && !node.classList.contains('thinking-bubble') && !meaningfulVisible) {
                    collapseNode(node);
                }
            });
        }

        function linkifyUrls(html) {
            let out = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/gi,
                '<a href="$2" target="_blank" rel="noopener">$1</a>');
            out = out.replace(/(?<!["=a-z])(https?:\/\/[^\s<>"']+)/gi, (match) => {
                const href = match.replace(/&amp;/g, '&');
                return `<a href="${href}" target="_blank" rel="noopener">${match}</a>`;
            });
            return out;
        }

        function renderChatMarkdown(text) {
            const raw = String(text || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
            if (!raw) return '(empty message)';
            const escaped = deps.helpers.escapeHtml(raw);
            const withInline = linkifyUrls(escaped
                .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
                .replace(/(?<![*])\*([^*]+?)\*(?!\*)/g, '<em>$1</em>')
                .replace(/`([^`]+)`/g, '<code>$1</code>'))
                .replace(/\[\u{1F4CE}\s*([^:\]]+):\s*([^\]]+)\]/gu,
                    '<span class="citation-badge" title="Past session: $2">📎 $1: $2</span>');

            const lines = withInline.split(/\r?\n/);
            const html = [];
            let inUl = false;
            let inOl = false;

            const closeList = () => {
                if (inUl) { html.push('</ul>'); inUl = false; }
                if (inOl) { html.push('</ol>'); inOl = false; }
            };

            for (let i = 0; i < lines.length; i++) {
                const rawLine = lines[i];
                const line = rawLine.trim();
                if (!line) {
                    closeList();
                    continue;
                }

                if (line.includes('|') && i + 1 < lines.length) {
                    const sep = lines[i + 1].trim();
                    if (/^[|\s:-]+$/.test(sep) && sep.includes('---')) {
                        closeList();
                        const heads = line.split('|').map((c) => c.trim()).filter(Boolean);
                        const isCalendarTable = heads.length > 0 && heads[0] === '#' && heads.includes('_event_id');
                        const isEmailTable = heads.length > 0 && heads[0] === '#' && heads.includes('_message_id') && !isCalendarTable;
                        const hiddenCols = (isEmailTable || isCalendarTable)
                            ? heads.reduce((acc, h, hi) => h.startsWith('_') ? [...acc, hi] : acc, [])
                            : [];
                        const emailMessageIdColIdx = isEmailTable ? heads.findIndex((h) => h === '_message_id') : -1;
                        const emailProviderColIdx = isEmailTable ? heads.findIndex((h) => h === '_provider') : -1;
                        const eventIdColIdx = isCalendarTable ? heads.findIndex((h) => h === '_event_id') : -1;
                        const providerColIdx = isCalendarTable ? heads.findIndex((h) => h === '_provider') : -1;
                        const startIsoColIdx = isCalendarTable ? heads.findIndex((h) => h === '_start_iso') : -1;
                        const endIsoColIdx = isCalendarTable ? heads.findIndex((h) => h === '_end_iso') : -1;
                        const tableClass = isEmailTable ? ' email-table' : isCalendarTable ? ' calendar-table' : '';
                        let table = `<div class="chat-table-wrap"><table class="chat-table${tableClass}"><thead><tr>`;
                        for (let hi = 0; hi < heads.length; hi++) {
                            if (hiddenCols.includes(hi)) continue;
                            table += `<th>${heads[hi]}</th>`;
                        }
                        table += '</tr></thead><tbody>';
                        i += 2;
                        while (i < lines.length) {
                            const row = lines[i].trim();
                            if (!row || !row.includes('|')) break;
                            const cells = row.split('|').map((c) => c.trim()).filter(Boolean);
                            const idx = (isEmailTable || isCalendarTable) && cells.length > 0 ? cells[0].trim() : '';
                            let rowAttr = '';
                            if (isEmailTable && /^\d+$/.test(idx)) {
                                rowAttr = ` data-email-index="${idx}"`;
                                if (emailMessageIdColIdx >= 0 && cells[emailMessageIdColIdx] && cells[emailMessageIdColIdx] !== '-') {
                                    rowAttr += ` data-message-id="${cells[emailMessageIdColIdx].replace(/"/g, '&quot;')}"`;
                                }
                                if (emailProviderColIdx >= 0 && cells[emailProviderColIdx] && cells[emailProviderColIdx] !== '-') {
                                    rowAttr += ` data-provider="${cells[emailProviderColIdx].replace(/"/g, '&quot;')}"`;
                                }
                            } else if (isCalendarTable && /^\d+$/.test(idx)) {
                                rowAttr = ` data-calendar-index="${idx}"`;
                                if (eventIdColIdx >= 0 && cells[eventIdColIdx]) {
                                    rowAttr += ` data-event-id="${cells[eventIdColIdx].replace(/"/g, '&quot;')}"`;
                                }
                                if (providerColIdx >= 0 && cells[providerColIdx]) {
                                    rowAttr += ` data-provider="${cells[providerColIdx].replace(/"/g, '&quot;')}"`;
                                }
                                if (startIsoColIdx >= 0 && cells[startIsoColIdx]) {
                                    rowAttr += ` data-start-iso="${cells[startIsoColIdx].replace(/"/g, '&quot;')}"`;
                                }
                                if (endIsoColIdx >= 0 && cells[endIsoColIdx]) {
                                    rowAttr += ` data-end-iso="${cells[endIsoColIdx].replace(/"/g, '&quot;')}"`;
                                }
                            }
                            table += `<tr${rowAttr}>`;
                            for (let ci = 0; ci < cells.length; ci++) {
                                if (hiddenCols.includes(ci)) continue;
                                const cell = cells[ci];
                                const statusClass = isEmailTable
                                    ? (cell === 'Unread' || cell === 'Ongelezen' ? ' email-status--unread'
                                    : cell === 'Read' || cell === 'Gelezen' ? ' email-status--read'
                                    : cell === 'Replied' || cell === 'Beantwoord' ? ' email-status--replied'
                                    : '')
                                    : '';
                                if (statusClass) {
                                    table += `<td><span class="email-status${statusClass}">${cell}</span></td>`;
                                } else {
                                    table += `<td>${cell}</td>`;
                                }
                            }
                            table += '</tr>';
                            i++;
                        }
                        table += '</tbody></table></div>';
                        html.push(table);
                        i--;
                        continue;
                    }
                }

                const isIndented = rawLine.length > line.length && (rawLine.startsWith('  ') || rawLine.startsWith('\t'));
                const bullet = line.match(/^[-*•]\s+(.+)/);
                if (bullet && !isIndented) {
                    if (inOl) { html.push('</ol>'); inOl = false; }
                    if (!inUl) { html.push('<ul>'); inUl = true; }
                    html.push(`<li>${bullet[1]}</li>`);
                    continue;
                }
                const numbered = line.match(/^(\d+)[.)\-]\s+(.+)/);
                if (numbered && !isIndented) {
                    if (inUl) { html.push('</ul>'); inUl = false; }
                    if (!inOl) { html.push('<ol>'); inOl = true; }
                    html.push(`<li>${numbered[2]}</li>`);
                    continue;
                }

                if (isIndented && (inUl || inOl)) {
                    const lastIdx = html.length - 1;
                    if (lastIdx >= 0 && html[lastIdx].endsWith('</li>')) {
                        html[lastIdx] = html[lastIdx].slice(0, -5) + '<br><span class="chat-detail">' + line + '</span></li>';
                        continue;
                    }
                }

                closeList();
                if (isIndented) {
                    html.push(`<p class="chat-detail">${line}</p>`);
                    continue;
                }

                const headerMatch = line.match(/^<strong>(.+)<\/strong>$/);
                if (headerMatch) {
                    html.push(`<p class="chat-section-header"><strong>${headerMatch[1]}</strong></p>`);
                    continue;
                }
                html.push(`<p>${line}</p>`);
            }

            closeList();
            return html.join('') || `<p>${withInline}</p>`;
        }

        function scrollChatToBottom() {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            requestAnimationFrame(() => {
                chatThread.scrollTop = chatThread.scrollHeight;
            });
        }

        function forceStickChatBottom(ms) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            forceStickBottomUntil = Date.now() + Math.max(500, Number(ms) || 0);
            scrollChatToBottom();
            setTimeout(scrollChatToBottom, 60);
            setTimeout(scrollChatToBottom, 220);
        }

        function parseWorkflowStatusMessage(text) {
            if (!deps.showFlowInThread) return null;
            const raw = String(text || '').trim();
            if (!raw) return null;
            const normalized = raw.replace(/^✓\s*/, '').trim().toLowerCase();
            if (normalized.startsWith('workflow started') || normalized.startsWith('escalating to full workflow')) {
                return { tone: 'working', label: 'Mobilizing agents', detail: 'to get the job done', animated: true };
            }
            if (normalized === 'workflow completed.' || normalized === 'workflow completed' || normalized.startsWith('workflow completed')) {
                return { tone: 'done', label: 'Agents finished', detail: 'result is ready', animated: false };
            }
            return null;
        }

        function buildWorkflowStatusDOM(bubble, status, meta) {
            bubble.className = `chat-bubble app workflow-status workflow-status--${status.tone}`;
            const avatar = document.createElement('div');
            avatar.className = 'chat-avatar';
            avatar.textContent = status.tone === 'done' ? '✓' : '⚡';
            bubble.appendChild(avatar);

            const content = document.createElement('div');
            content.className = 'chat-bubble-content workflow-status-content';
            const row = document.createElement('div');
            row.className = 'workflow-status-row';
            const pulse = document.createElement('span');
            pulse.className = 'workflow-status-pulse';
            row.appendChild(pulse);
            const label = document.createElement('span');
            label.className = 'workflow-status-label';
            label.textContent = status.label || 'Workflow update';
            row.appendChild(label);
            if (status.detail) {
                const detail = document.createElement('span');
                detail.className = 'workflow-status-detail';
                detail.textContent = status.detail;
                row.appendChild(detail);
            }
            if (status.animated) {
                const dots = document.createElement('span');
                dots.className = 'workflow-status-dots';
                dots.setAttribute('aria-hidden', 'true');
                dots.innerHTML = '<span></span><span></span><span></span>';
                row.appendChild(dots);
            }
            content.appendChild(row);
            if (meta) {
                const metaEl = document.createElement('div');
                metaEl.className = 'chat-bubble-meta workflow-status-meta';
                metaEl.textContent = meta;
                content.appendChild(metaEl);
            }
            bubble.appendChild(content);
        }

        function buildChatBubbleDOM(role, text, meta = '') {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            const bubble = document.createElement('div');
            const normalizedRole = role === 'user' || role === 'app' ? role : 'system';
            const isHandoffBubble = normalizedRole === 'app'
                && String(meta || '').trim().toLowerCase() === 'workflow'
                && String(text || '').trim().startsWith('↪');
            if (normalizedRole === 'system') {
                const workflowStatus = parseWorkflowStatusMessage(text);
                if (workflowStatus) {
                    buildWorkflowStatusDOM(bubble, workflowStatus, meta);
                    chatThread.appendChild(bubble);
                    ensureChatThreadVisibleState();
                    return;
                }
            }
            bubble.className = `chat-bubble ${normalizedRole}${isHandoffBubble ? ' handoff-bubble' : ''}`;
            const avatar = document.createElement('div');
            avatar.className = 'chat-avatar';
            avatar.textContent = normalizedRole === 'user' ? '👤' : (normalizedRole === 'app' ? '⚡' : '•');
            bubble.appendChild(avatar);
            const content = document.createElement('div');
            content.className = 'chat-bubble-content';
            const roleEl = document.createElement('div');
            roleEl.className = 'chat-bubble-role';
            roleEl.textContent = normalizedRole === 'user'
                ? 'You'
                : (normalizedRole === 'app' ? (String(meta || '').trim() || 'Agent') : 'System');
            const textEl = document.createElement('div');
            textEl.className = 'chat-bubble-text';
            textEl.innerHTML = renderChatMarkdown(text);
            content.appendChild(roleEl);
            content.appendChild(textEl);
            if (meta) {
                const metaEl = document.createElement('div');
                metaEl.className = 'chat-bubble-meta';
                metaEl.textContent = meta;
                content.appendChild(metaEl);
            }
            bubble.appendChild(content);
            chatThread.appendChild(bubble);
            ensureChatThreadVisibleState();
        }

        function shouldKeepThinkingExpanded() {
            return !!(deps.state.getAppSettings?.() && deps.state.getAppSettings().show_thinking);
        }

        function ensureActiveThinkingBubble() {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return { bubble: null, content: null };
            let bubble = document.getElementById('chat-thinking');
            if (!bubble) {
                bubble = document.createElement('div');
                bubble.className = 'chat-bubble app thinking-bubble';
                bubble.id = 'chat-thinking';
                chatThread.appendChild(bubble);
            }
            bubble.className = 'chat-bubble app thinking-bubble';
            let avatar = bubble.querySelector('.chat-avatar');
            if (!avatar) {
                avatar = document.createElement('div');
                avatar.className = 'chat-avatar';
                bubble.insertBefore(avatar, bubble.firstChild || null);
            }
            avatar.textContent = '⚡';
            let content = bubble.querySelector('.chat-bubble-content');
            if (!content) {
                content = document.createElement('div');
                content.className = 'chat-bubble-content';
                bubble.appendChild(content);
            }
            ensureChatThreadVisibleState();
            return { bubble, content };
        }

        function setThinkingActivityState(details, active = true, options = {}) {
            if (!(details instanceof HTMLElement)) return;
            const body = details.querySelector('.thinking-body');
            details.classList.toggle('thinking-block-active', !!active);
            if (body) body.classList.toggle('thinking-body-active', !!active);
            const previousTimer = Number(details.dataset.activityTimer || 0);
            if (previousTimer) {
                window.clearTimeout(previousTimer);
                delete details.dataset.activityTimer;
            }
            if (active && Number(options.duration || 0) > 0) {
                const timerId = window.setTimeout(() => {
                    details.classList.remove('thinking-block-active');
                    if (body) body.classList.remove('thinking-body-active');
                    delete details.dataset.activityTimer;
                }, Number(options.duration));
                details.dataset.activityTimer = String(timerId);
            }
        }

        function ensureActiveThinkingDots() {
            const { bubble, content } = ensureActiveThinkingBubble();
            if (!bubble || !content) return { bubble: null, content: null };
            const activeBodyText = String(content.querySelector('.thinking-body')?.textContent || '').replace(/\s+/g, '').trim();
            if (!activeBodyText) {
                content.innerHTML = '<div class="thinking-dots"><span></span><span></span><span></span></div>';
            }
            return { bubble, content };
        }

        function ensureActiveThinkingDetails(summaryText = 'Thinking…', expanded = shouldKeepThinkingExpanded()) {
            const { bubble, content } = ensureActiveThinkingBubble();
            if (!bubble || !content) return { bubble: null, details: null, body: null };
            let details = content.querySelector('.thinking-block');
            let body = details?.querySelector('.thinking-body');
            if (!details || !body) {
                content.innerHTML = '';
                details = document.createElement('details');
                details.className = 'thinking-block';
                const summary = document.createElement('summary');
                const nextBody = document.createElement('div');
                nextBody.className = 'thinking-body';
                details.appendChild(summary);
                details.appendChild(nextBody);
                content.appendChild(details);
                body = nextBody;
            }
            const summary = details.querySelector('summary');
            if (summary) summary.textContent = summaryText;
            details.open = expanded;
            setThinkingActivityState(details, true);
            if (bubble.dataset.compactHidden === '1') {
                delete bubble.dataset.compactHidden;
                bubble.style.display = '';
                bubble.style.height = '';
                bubble.style.margin = '';
                bubble.style.padding = '';
            }
            return { bubble, details, body };
        }

        function finalizeActiveThinkingBubble() {
            const bubble = document.getElementById('chat-thinking');
            if (!bubble) return;
            const details = bubble.querySelector('.thinking-block');
            const body = details?.querySelector('.thinking-body');
            const blockText = String(body?.textContent || '').replace(/\s+/g, '').trim();
            if (!details || !blockText) {
                bubble.remove();
                return;
            }
            if (bubble.dataset.compactHidden === '1') {
                delete bubble.dataset.compactHidden;
                bubble.style.display = '';
                bubble.style.height = '';
                bubble.style.margin = '';
                bubble.style.padding = '';
            }
            setThinkingActivityState(details, false);
            bubble.removeAttribute('id');
        }

        function appendChatBubble(role, text, meta = '') {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            const currentHandlingRunId = deps.state.getCurrentHandlingRunId?.() || '';
            const activeRunId = deps.state.getActiveRunId?.() || '';
            const runs = deps.state.getRuns?.();
            if (!currentHandlingRunId || currentHandlingRunId === activeRunId) {
                finalizeActiveThinkingBubble();
            }
            if (!deps.showFlowInThread && role === 'user') {
                chatThread.querySelectorAll('.chat-bubble.agent-activity, .chat-bubble.workflow-status').forEach((node) => node.remove());
            }
            const targetRunId = currentHandlingRunId || activeRunId;
            if (targetRunId && runs?.has(targetRunId)) {
                if (!deps.showFlowInThread) {
                    const run = runs.get(targetRunId);
                    run._chatBubbles = (run._chatBubbles || []).filter((bubble) => {
                        const type = bubble?.type || 'chat';
                        if (type === 'plan' || type === 'step') return false;
                        if (type === 'chat' && bubble?.role === 'system') {
                            const tx = String(bubble?.text || '').toLowerCase();
                            if (tx.includes('workflow started') || tx.includes('workflow completed')) return false;
                        }
                        return true;
                    });
                }
                runs.get(targetRunId)._chatBubbles.push({ type: 'chat', role, text, meta });
            } else {
                deps.state.getPendingPreRunChatBubbles?.().push({ type: 'chat', role, text, meta });
            }
            if (!currentHandlingRunId || currentHandlingRunId === activeRunId) {
                buildChatBubbleDOM(role, text, meta);
                scrollChatToBottom();
            }
        }

        function fileIcon(name) {
            const ext = (name || '').split('.').pop().toLowerCase();
            const icons = {
                pdf: '📄', doc: '📝', docx: '📝', xls: '📊', xlsx: '📊', csv: '📊',
                ppt: '📎', pptx: '📎', txt: '📄', md: '📄', json: '📄', xml: '📄',
                png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️', webp: '🖼️',
                zip: '📦', rar: '📦', '7z': '📦', tar: '📦', gz: '📦',
                py: '🐍', js: '📜', html: '🌐', css: '🎨',
                mp3: '🎵', wav: '🎵', mp4: '🎬', avi: '🎬',
            };
            return icons[ext] || '📁';
        }

        function buildReportBubbleDOM(goalText, content, outputFiles = []) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            const raw = String(content || '').trim();
            if (!raw) return;
            const reportId = `${String(goalText || '').trim()}::${raw.length}::${raw.slice(0, 160)}`;
            chatThread.querySelectorAll('.chat-bubble.report-bubble').forEach((el) => {
                if ((el.dataset.reportId || '') === reportId) el.remove();
            });
            const bubble = document.createElement('div');
            bubble.className = 'chat-bubble app report-bubble';
            bubble.dataset.reportId = reportId;
            const avatar = document.createElement('div');
            avatar.className = 'chat-avatar';
            avatar.textContent = '📋';
            bubble.appendChild(avatar);
            const wrapper = document.createElement('div');
            wrapper.className = 'chat-bubble-content report-content-inline';
            const roleEl = document.createElement('div');
            roleEl.className = 'chat-bubble-role';
            roleEl.textContent = goalText ? `Report — ${goalText}` : 'Final Report';
            const bodyEl = document.createElement('div');
            bodyEl.className = 'chat-bubble-text report-body-inline';
            try {
                bodyEl.innerHTML = deps.helpers.formatFinalReport(raw);
            } catch (error) {
                console.error('Final report render failed:', error);
                bodyEl.innerHTML = deps.helpers.escapeHtml(raw).replace(/\r?\n/g, '<br>');
            }
            wrapper.appendChild(roleEl);
            wrapper.appendChild(bodyEl);
            const files = Array.isArray(outputFiles) ? outputFiles : [];
            if (files.length > 0) {
                const filesEl = document.createElement('div');
                filesEl.className = 'report-output-files';
                const filesTitle = document.createElement('div');
                filesTitle.className = 'report-files-title';
                filesTitle.textContent = `📁 Output Files (${files.length})`;
                filesEl.appendChild(filesTitle);
                files.forEach((file) => {
                    const row = document.createElement('a');
                    row.className = 'report-file-link';
                    row.href = '#';
                    row.addEventListener('click', async (event) => {
                        event.preventDefault();
                        try {
                            const res = await deps.helpers.authFetch(`${deps.helpers.getBackendUrl()}/api/output/${encodeURIComponent(file.name)}`);
                            if (!res.ok) throw new Error(`HTTP ${res.status}`);
                            const blob = await res.blob();
                            const url = URL.createObjectURL(blob);
                            const tmp = document.createElement('a');
                            tmp.href = url;
                            tmp.download = file.name;
                            document.body.appendChild(tmp);
                            tmp.click();
                            tmp.remove();
                            URL.revokeObjectURL(url);
                        } catch (error) {
                            deps.helpers.showToast('error', `Download failed: ${error.message}`, 5000);
                        }
                    });
                    const icon = fileIcon(file.name);
                    const sizeStr = file.size >= 1048576 ? `${(file.size / 1048576).toFixed(1)} MB`
                        : file.size >= 1024 ? `${(file.size / 1024).toFixed(1)} KB`
                        : `${file.size} B`;
                    row.innerHTML = `<span class="report-file-icon">${icon}</span>`
                        + `<span class="report-file-name">${deps.helpers.escapeHtml(file.name)}</span>`
                        + `<span class="report-file-size">${sizeStr}</span>`;
                    filesEl.appendChild(row);
                });
                wrapper.appendChild(filesEl);
            }
            bubble.appendChild(wrapper);
            chatThread.appendChild(bubble);
            ensureChatThreadVisibleState();
        }

        function appendReportBubble(goalText, content, outputFiles = []) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            const raw = String(content || '').trim();
            if (!raw) return;
            const bubbleData = {
                type: 'report',
                goalText: String(goalText || ''),
                content: raw,
                outputFiles: Array.isArray(outputFiles) ? outputFiles : [],
            };
            const currentHandlingRunId = deps.state.getCurrentHandlingRunId?.() || '';
            const activeRunId = deps.state.getActiveRunId?.() || '';
            const runs = deps.state.getRuns?.();
            const targetRunId = currentHandlingRunId || activeRunId;
            if (targetRunId && runs?.has(targetRunId)) {
                runs.get(targetRunId)._chatBubbles.push(bubbleData);
            } else {
                deps.state.getPendingPreRunChatBubbles?.().push(bubbleData);
            }
            if (!currentHandlingRunId || currentHandlingRunId === activeRunId) {
                buildReportBubbleDOM(goalText, raw, outputFiles);
                scrollChatToBottom();
            }
        }

        function showThinkingBubble() {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            ensureActiveThinkingDots();
            ensureChatThreadVisibleState();
            scrollChatToBottom();
        }

        function removeThinkingBubble(force = false) {
            const el = document.getElementById('chat-thinking');
            if (!el) return;
            const hasDots = !!el.querySelector('.thinking-dots');
            const block = el.querySelector('.thinking-block');
            const body = block?.querySelector('.thinking-body');
            const blockText = String(body?.textContent || '').replace(/\s+/g, '').trim();
            if (!force && block && !hasDots && blockText) {
                finalizeActiveThinkingBubble();
                return;
            }
            el.remove();
        }

        function normalizeThinkingText(text, trimLeading = false, trimTrailing = false) {
            let out = String(text || '').replace(/\r/g, '').replace(/\u00a0/g, ' ');
            out = out.split('\n').map((line) => line.replace(/[ \t]+$/g, '')).join('\n');
            if (trimLeading) out = out.replace(/^\s*\n+/, '');
            out = out.replace(/\n(?:[ \t]*\n)+/g, '\n');
            if (trimTrailing) out = out.replace(/\s+$/g, '');
            return out;
        }

        function getThinkingBodyText(body) {
            if (!body) return '';
            const rawText = typeof body.dataset?.rawText === 'string'
                ? body.dataset.rawText
                : body.textContent;
            return normalizeThinkingText(rawText, true, false);
        }

        function setThinkingBodyText(body, text, options = {}) {
            if (!body) return '';
            const normalized = normalizeThinkingText(text, true, !!options.trimTrailing);
            body.dataset.rawText = normalized;
            if (!options.asLines) {
                body.textContent = normalized;
                return normalized;
            }
            body.replaceChildren();
            if (!normalized) return normalized;
            const animateFromIndex = Number.isInteger(options.animateFromIndex)
                ? options.animateFromIndex
                : Number.MAX_SAFE_INTEGER;
            const fragment = document.createDocumentFragment();
            normalized.split('\n').forEach((line, index) => {
                const lineEl = document.createElement('div');
                lineEl.className = 'thinking-line';
                if (index >= animateFromIndex) {
                    lineEl.classList.add('thinking-line-enter');
                }
                lineEl.textContent = line || ' ';
                fragment.appendChild(lineEl);
            });
            body.appendChild(fragment);
            return normalized;
        }

        function appendProcessThinkingLine(line, runId = '') {
            const appSettings = deps.state.getAppSettings?.();
            if (!appSettings?.show_thinking) return;
            const text = String(line || '').trim();
            if (!text) return;
            const targetRunId = String(runId || deps.state.getCurrentHandlingRunId?.() || deps.state.getActiveRunId?.() || '').trim();
            const runs = deps.state.getRuns?.();
            const targetRun = targetRunId && runs?.has(targetRunId) ? runs.get(targetRunId) : null;
            const thinkingState = deps.state.getThinkingState?.() || { body: null, details: null };
            let details = targetRun ? targetRun._thinkingDetails : thinkingState.details;
            let body = targetRun ? targetRun._thinkingBody : thinkingState.body;
            if (!details || !body) {
                details = document.createElement('details');
                details.className = 'thinking-block';
                const summary = document.createElement('summary');
                summary.textContent = 'Thought process';
                body = document.createElement('div');
                body.className = 'thinking-body';
                details.appendChild(summary);
                details.appendChild(body);
                if (targetRun?._streamEntry) {
                    const streamBody = targetRun._streamEntry.querySelector('.stream-body');
                    if (streamBody) targetRun._streamEntry.insertBefore(details, streamBody);
                    else targetRun._streamEntry.appendChild(details);
                } else if (deps.refs.chatThread) {
                    const thinking = ensureActiveThinkingDetails('Thought process', true);
                    details = thinking.details;
                    body = thinking.body;
                    details.style.display = '';
                    forceStickChatBottom(4500);
                }
            }
            const existing = getThinkingBodyText(body);
            const existingLineCount = existing ? existing.split('\n').length : 0;
            const addition = existing ? `\n${text}` : text;
            setThinkingBodyText(body, existing + addition, {
                asLines: true,
                animateFromIndex: existingLineCount,
            });
            details.style.display = '';
            setThinkingActivityState(details, true);
            if (targetRun) {
                targetRun._thinkingBody = body;
                targetRun._thinkingDetails = details;
            } else {
                deps.state.setThinkingState?.(body, details);
            }
            scrollChatToBottom();
        }

        const agentDisplayNames = {
            planner: 'Planner', researcher: 'Researcher', curator: 'Curator',
            executor: 'Executor', validator: 'Validator', reportout: 'Report',
        };
        const agentActivityIcons = {
            planner: '🧠', researcher: '🔍', curator: '📋',
            executor: '⚙️', validator: '✅', reportout: '📊',
        };

        function buildAgentActivityLabelHtml(displayName, message, stateIcon = '') {
            const messageParts = [];
            if (stateIcon) {
                messageParts.push(`<span class="agent-activity-state-icon">${stateIcon}</span>`);
            }
            if (message) {
                messageParts.push(message);
            }
            const messageHtml = messageParts.length
                ? `<span class="agent-activity-message">${messageParts.join(' ')}</span>`
                : '';
            return `<span class="agent-activity-label"><strong class="agent-activity-name">${displayName}</strong>${messageHtml}</span>`;
        }

        function appendAgentActivityBubble(agent, state, msg) {
            const chatThread = deps.refs.chatThread;
            if (!deps.showFlowInThread || !chatThread) return;
            ensureChatThreadVisibleState();
            const bubbleId = `agent-activity-${agent}`;
            let bubble = document.getElementById(bubbleId);
            const icon = agentActivityIcons[agent] || '🤖';
            const displayName = agentDisplayNames[agent] || deps.helpers.escapeHtml(agent);
            const escapedMsg = msg ? deps.helpers.escapeHtml(msg) : '';

            function updateFlowMarkers() {
                const allBubbles = chatThread.querySelectorAll('.chat-bubble.agent-activity:not(.plan-bubble):not(.step-bubble)');
                allBubbles.forEach((item, index) => {
                    item.classList.toggle('agent-flow-start', index === 0);
                    item.classList.toggle('agent-flow-end', index === allBubbles.length - 1 && !item.classList.contains('active'));
                });
            }

            if (state === 'active') {
                if (!bubble) {
                    removeThinkingBubble();
                    bubble = document.createElement('div');
                    bubble.id = bubbleId;
                    bubble.className = 'chat-bubble agent-activity active';
                    const content = document.createElement('div');
                    content.className = 'agent-activity-content';
                    content.innerHTML = `<span class="agent-activity-icon">${icon}</span>`
                        + buildAgentActivityLabelHtml(displayName, escapedMsg)
                        + '<span class="agent-activity-dots"><span></span><span></span><span></span></span>';
                    bubble.appendChild(content);
                    chatThread.appendChild(bubble);
                } else {
                    const label = bubble.querySelector('.agent-activity-label');
                    if (label) label.outerHTML = buildAgentActivityLabelHtml(displayName, escapedMsg);
                    bubble.className = 'chat-bubble agent-activity active';
                }
                updateFlowMarkers();
                scrollChatToBottom();
            } else if (state === 'done' || state === 'skip' || state === 'error') {
                if (bubble) {
                    const stateIcon = state === 'done' ? '✓' : state === 'error' ? '✗' : '—';
                    const stateClass = state === 'error' ? 'error' : 'completed';
                    bubble.className = `chat-bubble agent-activity ${stateClass}`;
                    const content = bubble.querySelector('.agent-activity-content');
                    if (content) {
                        content.innerHTML = `<span class="agent-activity-icon">${icon}</span>`
                            + buildAgentActivityLabelHtml(displayName, escapedMsg, stateIcon);
                    }
                    updateFlowMarkers();
                }
            }
        }

        function clearAgentActivityBubbles(keepAgents = []) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            const keepIds = new Set((Array.isArray(keepAgents) ? keepAgents : [])
                .map((agent) => `agent-activity-${String(agent || '').trim().toLowerCase()}`));
            chatThread.querySelectorAll('.chat-bubble.agent-activity:not(.plan-bubble):not(.step-bubble)').forEach((bubble) => {
                if (keepIds.has(String(bubble.id || '').trim().toLowerCase())) return;
                bubble.remove();
            });
        }

        function buildPlanBubbleDOM(steps) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread || !steps.length) return;
            ensureChatThreadVisibleState();
            const bubble = document.createElement('div');
            bubble.className = 'chat-bubble agent-activity plan-bubble';
            const content = document.createElement('div');
            content.className = 'agent-activity-content plan-content';
            const header = document.createElement('div');
            header.className = 'plan-header';
            header.innerHTML = `<span class="agent-activity-icon">🧠</span> <strong>Plan</strong> — ${steps.length} step${steps.length !== 1 ? 's' : ''}`;
            content.appendChild(header);
            const list = document.createElement('ol');
            list.className = 'plan-steps-list';
            steps.forEach((step) => {
                const li = document.createElement('li');
                li.textContent = typeof step === 'string' ? step : (step.step || JSON.stringify(step));
                list.appendChild(li);
            });
            content.appendChild(list);
            bubble.appendChild(content);
            chatThread.appendChild(bubble);
            scrollChatToBottom();
        }

        function appendPlanBubble(steps) {
            const chatThread = deps.refs.chatThread;
            if (!deps.showFlowInThread || !chatThread || !steps.length) return;
            const currentHandlingRunId = deps.state.getCurrentHandlingRunId?.() || '';
            const activeRunId = deps.state.getActiveRunId?.() || '';
            const runs = deps.state.getRuns?.();
            const targetRunId = currentHandlingRunId || activeRunId;
            if (targetRunId && runs?.has(targetRunId)) {
                runs.get(targetRunId)._chatBubbles.push({ type: 'plan', steps: steps.map((step) => typeof step === 'string' ? step : (step.step || JSON.stringify(step))) });
            }
            if (!currentHandlingRunId || currentHandlingRunId === activeRunId) {
                buildPlanBubbleDOM(steps);
            }
        }

        function buildStepBubbleDOM(stepIndex, totalSteps, description, isReplay = false) {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            ensureChatThreadVisibleState();
            const bubble = document.createElement('div');
            bubble.className = isReplay
                ? 'chat-bubble agent-activity step-bubble completed'
                : 'chat-bubble agent-activity step-bubble step-active';
            const content = document.createElement('div');
            content.className = 'agent-activity-content';
            const stepNum = (stepIndex || 0) + 1;
            const total = totalSteps || '?';
            const dotsHtml = isReplay ? '' : '<span class="agent-activity-dots"><span></span><span></span><span></span></span>';
            content.innerHTML = `<span class="agent-activity-icon">⚙️</span>`
                + `<span class="agent-activity-label"><strong>Step ${stepNum}/${total}</strong> — ${deps.helpers.escapeHtml(description || '')}</span>`
                + dotsHtml;
            bubble.appendChild(content);
            chatThread.appendChild(bubble);
            scrollChatToBottom();
        }

        function appendStepBubble(stepIndex, totalSteps, description) {
            const chatThread = deps.refs.chatThread;
            if (!deps.showFlowInThread || !chatThread) return;
            const currentHandlingRunId = deps.state.getCurrentHandlingRunId?.() || '';
            const activeRunId = deps.state.getActiveRunId?.() || '';
            const runs = deps.state.getRuns?.();
            const targetRunId = currentHandlingRunId || activeRunId;
            if (targetRunId && runs?.has(targetRunId)) {
                runs.get(targetRunId)._chatBubbles.push({ type: 'step', stepIndex, totalSteps, description });
            }
            if (!currentHandlingRunId || currentHandlingRunId === activeRunId) {
                finalizeStepBubbles();
                buildStepBubbleDOM(stepIndex, totalSteps, description, false);
            }
        }

        function formatLiveStepResult(text, maxLen = 2200) {
            const raw = String(text || '').trim();
            if (!raw) return '(no output received)';
            if (raw.length <= maxLen) return raw;
            return raw.slice(0, maxLen) + '\n\n[truncated for live view; full result is in the final report]';
        }

        function normalizeWorkflowTextForCompare(text) {
            return String(text || '')
                .replace(/\r\n/g, '\n')
                .replace(/[ \t]+/g, ' ')
                .replace(/\n{3,}/g, '\n\n')
                .trim();
        }

        function isDuplicateWorkflowResult(finalReport, steps) {
            const normalizedFinal = normalizeWorkflowTextForCompare(finalReport);
            if (!normalizedFinal) return false;
            const lastStepResult = [...(Array.isArray(steps) ? steps : [])]
                .reverse()
                .map((step) => String(step?.result || '').trim())
                .find(Boolean) || '';
            if (!lastStepResult) return false;
            return normalizeWorkflowTextForCompare(lastStepResult) === normalizedFinal;
        }

        function finalizeStepBubbles() {
            const chatThread = deps.refs.chatThread;
            if (!chatThread) return;
            chatThread.querySelectorAll('.step-bubble.step-active').forEach((bubble) => {
                bubble.classList.remove('step-active', 'active');
                bubble.classList.add('completed');
                const dots = bubble.querySelector('.agent-activity-dots');
                if (dots) dots.remove();
            });
        }

        function bindUi() {
            const chatThread = deps.refs.chatThread;
            if (!chatThread || chatObserverBound) return;
            chatObserverBound = true;
            let chatAutoScroll = true;
            chatThread.addEventListener('scroll', () => {
                const gap = chatThread.scrollHeight - chatThread.scrollTop - chatThread.clientHeight;
                chatAutoScroll = gap < 80;
            }, { passive: true });
            const chatObs = new MutationObserver(() => {
                compactChatThreadLayout();
                if (chatAutoScroll || Date.now() < forceStickBottomUntil) scrollChatToBottom();
            });
            chatObs.observe(chatThread, { childList: true, subtree: true, characterData: true });
        }

        return {
            bindUi,
            ensureChatThreadVisibleState,
            compactChatThreadLayout,
            scrollChatToBottom,
            forceStickChatBottom,
            renderChatMarkdown,
            buildChatBubbleDOM,
            shouldKeepThinkingExpanded,
            ensureActiveThinkingDetails,
            buildReportBubbleDOM,
            appendChatBubble,
            appendReportBubble,
            showThinkingBubble,
            removeThinkingBubble,
            normalizeThinkingText,
            getThinkingBodyText,
            setThinkingBodyText,
            setThinkingActivityState,
            appendProcessThinkingLine,
            appendAgentActivityBubble,
            clearAgentActivityBubbles,
            buildPlanBubbleDOM,
            appendPlanBubble,
            buildStepBubbleDOM,
            appendStepBubble,
            formatLiveStepResult,
            normalizeWorkflowTextForCompare,
            isDuplicateWorkflowResult,
            finalizeStepBubbles,
        };
    };
})();