/* Chat UI: SSE streaming, message rendering, markdown */

const Chat = (() => {
    let eventSource = null;
    let currentAssistantEl = null;
    let currentAssistantContent = '';
    let _streamRawMode = localStorage.getItem('anteroom_stream_raw_mode') === 'true';
    let _rewindPosition = null;
    let _rewindMsgEl = null;
    let _lastSentText = '';
    let _pendingUserMessages = [];  // FIFO queue of {el, text} for SSE correlation
    let _streamAbortController = null;  // AbortController for current SSE fetch
    let _conversationType = 'chat';
    const _UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

    // Remote collaboration state
    let _remoteAssistantEl = null;
    let _remoteAssistantContent = '';

    // Lifecycle phase tracking
    let _thinkingPhase = '';
    let _streamingChars = 0;
    let _phaseStartTime = 0;
    let _lastChunkTime = 0;
    let _phaseElapsedInterval = null;
    let _stallCheckInterval = null;
    let _streamWatchdog = null;
    const _STALL_THRESHOLD_MS = 5000;
    const _PHASE_ELAPSED_DELAY_MS = 1500;
    const _WATCHDOG_TIMEOUT_MS = 45000;

    // Configure marked for safe link rendering (marked v15 passes token object)
    const renderer = new marked.Renderer();
    const originalLink = renderer.link.bind(renderer);
    function _safeHref(url) {
        var s = String(url).trim();
        if (/^(javascript|data|vbscript):/i.test(s)) return '#';
        if (/^(https?:|mailto:|\/(?!\/)|#)/i.test(s)) return s;
        return '#';
    }
    renderer.link = function(token) {
        try {
            const html = originalLink(token);
            if (!html) throw new Error('empty');
            return html.replace('<a ', '<a target="_blank" rel="noopener noreferrer" ');
        } catch {
            const href = _safeHref((token && token.href) || (typeof token === 'string' ? token : ''));
            const text = (token && token.text) || href;
            return `<a href="${DOMPurify.sanitize(href)}" target="_blank" rel="noopener noreferrer">${DOMPurify.sanitize(text)}</a>`;
        }
    };
    marked.use({ renderer });

    function init() {
        const sendBtn = document.getElementById('btn-send');
        const stopBtn = document.getElementById('btn-stop');
        const input = document.getElementById('message-input');

        sendBtn.addEventListener('click', sendMessage);
        stopBtn.addEventListener('click', stopGeneration);

        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });

        input.addEventListener('input', autoResizeInput);

        initRewindModal();
    }

    function isRawMode() { return _streamRawMode; }

    function setRawMode(val) {
        _streamRawMode = val;
        localStorage.setItem('anteroom_stream_raw_mode', val ? 'true' : 'false');
    }

    function setConversationType(type) {
        _conversationType = type || 'chat';
        const input = document.getElementById('message-input');
        const attachBtn = document.getElementById('btn-attach');
        // Keep backward compat for note/document placeholders
        if (_conversationType === 'note') {
            input.placeholder = 'Add entry...';
            if (attachBtn) attachBtn.style.display = 'none';
        } else if (_conversationType === 'document') {
            input.placeholder = 'Add content...';
            if (attachBtn) attachBtn.style.display = 'none';
        } else {
            input.placeholder = 'Type a message...';
            if (attachBtn) attachBtn.style.display = '';
        }
    }

    function autoResizeInput() {
        const input = document.getElementById('message-input');
        input.style.height = 'auto';
        input.style.height = Math.min(input.scrollHeight, 200) + 'px';
    }

    async function sendMessage() {
        const input = document.getElementById('message-input');
        const text = input.value.trim();
        if (!text) return;

        const conversationId = App.state.currentConversationId;
        if (!conversationId) {
            const payload = { type: _conversationType };
            if (App.state.currentSpaceId) payload.space_id = App.state.currentSpaceId;
            const conv = await App.api('/api/conversations', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload),
            });
            App.state.currentConversationId = conv.id;
            App.state.currentConversationType = conv.type || 'chat';
            _conversationType = App.state.currentConversationType;
            Sidebar.refresh();
        }

        // Note/document mode: POST to /entries endpoint, no AI streaming
        if (_conversationType === 'note' || _conversationType === 'document') {
            input.value = '';
            input.style.height = 'auto';
            try {
                const msg = await App.api(
                    `/api/conversations/${App.state.currentConversationId}/entries`,
                    {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ content: text }),
                    }
                );
                _appendNoteEntry(msg);
                scrollToBottom();
            } catch (err) {
                showToast('Failed to add entry: ' + err.message);
            }
            return;
        }

        _lastSentText = text;
        const msgEl = appendMessage('user', text);
        _pendingUserMessages.push({ el: msgEl, text: text });
        input.value = '';
        input.style.height = 'auto';

        const files = Attachments.getFiles();
        Attachments.clear();

        // Collect source references
        const sourceRefs = (typeof Sources !== 'undefined' && Sources.hasAttached()) ? Sources.getAttachedSources() : {};
        if (typeof Sources !== 'undefined') Sources.clearAttached();

        let body;
        let headers = { 'X-CSRF-Token': App._getCsrfToken(), 'X-Client-Id': App.state.clientId };
        if (files.length > 0) {
            const formData = new FormData();
            formData.append('message', text);
            files.forEach(f => formData.append('files', f));
            if (App.state.isPlanMode) formData.append('plan_mode', 'true');
            if (sourceRefs.source_ids && sourceRefs.source_ids.length > 0) {
                sourceRefs.source_ids.forEach(id => formData.append('source_ids', id));
            }
            if (sourceRefs.source_tag) formData.append('source_tag', sourceRefs.source_tag);
            if (sourceRefs.source_group_id) formData.append('source_group_id', sourceRefs.source_group_id);
            body = formData;
        } else {
            const payload = { message: text };
            if (App.state.isPlanMode) payload.plan_mode = true;
            if (sourceRefs.source_ids && sourceRefs.source_ids.length > 0) payload.source_ids = sourceRefs.source_ids;
            if (sourceRefs.source_tag) payload.source_tag = sourceRefs.source_tag;
            if (sourceRefs.source_group_id) payload.source_group_id = sourceRefs.source_group_id;
            body = JSON.stringify(payload);
            headers['Content-Type'] = 'application/json';
        }

        if (App.state.isStreaming) {
            try {
                const response = await fetch(`/api/conversations/${App.state.currentConversationId}/chat`, {
                    method: 'POST',
                    headers,
                    body,
                    credentials: 'same-origin',
                });
                if (response.ok) {
                    const ct = response.headers.get('content-type') || '';
                    if (ct.includes('application/json')) {
                        const result = await response.json();
                        if (result.status === 'queued') {
                            const badge = document.createElement('span');
                            badge.className = 'queued-badge';
                            badge.textContent = 'queued';
                            msgEl.querySelector('.message-role').appendChild(badge);
                        }
                        return;
                    }
                } else {
                    let detail = `Queue failed (${response.status})`;
                    try {
                        const err = await response.json();
                        if (err.detail) detail = err.detail;
                    } catch (_) { /* ignore parse errors */ }
                    // Queue failed — the server stream is likely dead.
                    // Reset streaming state so the user can send messages normally.
                    setStreaming(false);
                    _pendingUserMessages = [];
                    showToast(detail);
                }
            } catch (e) {
                // Queue request itself failed — reset streaming state
                setStreaming(false);
                _pendingUserMessages = [];
                showToast('Failed to queue message');
            }
            return;
        }

        await streamChatResponse(App.state.currentConversationId, body, headers);
    }

    function abortStream() {
        if (_streamAbortController) {
            _streamAbortController.abort();
            _streamAbortController = null;
        }
    }

    async function streamChatResponse(conversationId, body, headers) {
        if (typeof App._debugLog === 'function') {
            App._debugLog('stream', 'Starting chat stream for ' + conversationId);
        }
        // Abort any in-flight stream before starting a new one
        abortStream();
        _streamAbortController = new AbortController();

        setStreaming(true);
        showThinking();

        if (!headers) {
            headers = {
                'Content-Type': 'application/json',
                'X-CSRF-Token': App._getCsrfToken(),
            };
        }
        headers['X-Client-Id'] = App.state.clientId;

        try {
            const response = await fetch(`/api/conversations/${conversationId}/chat`, {
                method: 'POST',
                headers,
                body,
                credentials: 'same-origin',
                signal: _streamAbortController.signal,
            });

            if (response.status === 401) {
                App._handle401();
                throw new Error('Session expired');
            }
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            currentAssistantContent = '';
            currentAssistantEl = appendMessage('assistant', '');

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let eventType = null;

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split('\n');
                buffer = lines.pop() || '';

                for (const line of lines) {
                    if (line.startsWith('event: ')) {
                        eventType = line.slice(7).trim();
                    } else if (line.startsWith('data: ') && eventType) {
                        try {
                            const data = JSON.parse(line.slice(6));
                            if (data && typeof data === 'object') {
                                handleSSEEvent(eventType, data);
                            }
                        } catch (e) {
                            console.warn('Failed to parse SSE data:', e);
                        }
                        eventType = null;
                    }
                }
            }
        } catch (err) {
            // Silently ignore aborted fetches (navigation, stop, new chat)
            if (err.name === 'AbortError') return;
            hideThinking();
            if (currentAssistantEl) {
                showError(currentAssistantEl, err.message);
            } else {
                showError(null, err.message);
            }
        } finally {
            _streamAbortController = null;
            hideThinking();
            _clearAllToolTimers();
            setStreaming(false);
            _pendingUserMessages = [];
        }
    }

    function handleSSEEvent(type, data) {
        _resetWatchdog();
        if (typeof App._debugLog === 'function' && type !== 'token') {
            const safe = {type: type};
            if (data.phase) safe.phase = data.phase;
            if (data.tool_name) safe.tool_name = data.tool_name;
            if (data.id) safe.id = data.id;
            App._debugLog('sse', 'event=' + type, safe);
        }
        switch (type) {
            case 'thinking':
                showThinking();
                break;
            case 'phase':
                updateThinkingPhase(data.phase, null);
                break;
            case 'retrying':
                updateThinkingPhase('retrying', data);
                break;
            case 'token':
                _hideThinkingKeepPhase();
                _webDedupFlush();
                _onStreamingToken(data.content ? data.content.length : 0);
                currentAssistantContent += data.content;
                renderAssistantContent();
                break;
            case 'tool_call_start':
                hideThinking();
                renderToolCallStart(data);
                break;
            case 'tool_call_end':
                renderToolCallEnd(data);
                break;
            case 'canvas_stream_start':
                hideThinking();
                Canvas.handleCanvasStreamStart(data);
                break;
            case 'canvas_streaming':
                Canvas.handleCanvasStreaming(data);
                break;
            case 'canvas_created':
                hideThinking();
                Canvas.handleCanvasCreated(data);
                break;
            case 'canvas_updated':
                hideThinking();
                Canvas.handleCanvasUpdated(data);
                break;
            case 'canvas_patched':
                hideThinking();
                Canvas.handleCanvasPatched(data);
                break;
            case 'title':
                Sidebar.updateTitle(App.state.currentConversationId, data.title);
                break;
            case 'queued_message':
                finalizeAssistant();
                currentAssistantContent = '';
                currentAssistantEl = appendMessage('assistant', '');
                document.querySelectorAll('.queued-badge').forEach(b => b.remove());
                break;
            case 'subagent_event':
                renderSubagentEvent(data);
                break;
            case 'plan_saved':
                if (App.state.isPlanMode && data.content) {
                    App._showPlanContent(data.content);
                }
                break;
            case 'user_message':
                if (_pendingUserMessages.length > 0 && typeof data.id === 'string'
                    && _UUID_RE.test(data.id) && Number.isInteger(data.position)) {
                    const pending = _pendingUserMessages.shift();
                    const userMsgData = { id: data.id, position: data.position, content: pending.text };
                    addMessageActions(pending.el, 'user', pending.text, userMsgData, { isLast: false });
                }
                break;
            case 'done':
                hideThinking();
                finalizeAssistant(data);
                break;
            case 'prompt_meta':
                if (data.sources_truncated) {
                    showToast('Source content was truncated to fit the 50KB limit.');
                }
                if (data.rag_status === 'ok' && data.rag_chunks > 0) {
                    showToast(`RAG: ${data.rag_chunks} relevant chunk(s) retrieved`);
                } else if (data.rag_status === 'no_results') {
                    showToast('RAG: no relevant context found');
                } else if (data.rag_status === 'failed') {
                    showToast('RAG: retrieval failed');
                }
                break;
            case 'budget_warning':
                showToast(data.message || 'Token budget warning');
                break;
            case 'error':
                hideThinking();
                if (currentAssistantEl) {
                    showError(currentAssistantEl, data.message);
                } else {
                    showError(null, data.message);
                }
                break;
        }
    }

    function renderAssistantContent() {
        if (!currentAssistantEl) return;
        const contentEl = currentAssistantEl.querySelector('.message-content');
        if (_streamRawMode) {
            contentEl.textContent = currentAssistantContent;
        } else {
            contentEl.innerHTML = renderMarkdown(currentAssistantContent);
            renderMath(contentEl);
            highlightCode(contentEl);
        }
        scrollToBottom();
    }

    function finalizeAssistant(doneData) {
        if (!currentAssistantEl) return;
        const contentEl = currentAssistantEl.querySelector('.message-content');
        contentEl.innerHTML = renderMarkdown(currentAssistantContent);
        renderMath(contentEl);
        addCodeCopyButtons(contentEl);
        let msgData = null;
        if (doneData && typeof doneData.assistant_message_id === 'string'
            && _UUID_RE.test(doneData.assistant_message_id) && Number.isInteger(doneData.assistant_message_position)) {
            msgData = { id: doneData.assistant_message_id, position: doneData.assistant_message_position };
        }
        addMessageActions(currentAssistantEl, 'assistant', currentAssistantContent, msgData, { isLast: true });
        currentAssistantEl = null;
        currentAssistantContent = '';
        scrollToBottom();
    }

    function renderMarkdown(text) {
        if (!text) return '';

        // Protect math blocks from markdown processing
        const mathBlocks = [];
        let protected_ = text;

        // Display math: $$...$$ and \[...\]
        protected_ = protected_.replace(/\$\$([\s\S]*?)\$\$/g, (match) => {
            mathBlocks.push(match);
            return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
        });
        protected_ = protected_.replace(/\\\[([\s\S]*?)\\\]/g, (match) => {
            mathBlocks.push(match);
            return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
        });

        // Inline math: $...$ and \(...\)
        protected_ = protected_.replace(/\$([^\$\n]+?)\$/g, (match) => {
            mathBlocks.push(match);
            return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
        });
        protected_ = protected_.replace(/\\\(([\s\S]*?)\\\)/g, (match) => {
            mathBlocks.push(match);
            return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
        });

        // Render markdown on the protected text
        let html = marked.parse(protected_);

        // Restore math blocks
        html = html.replace(/%%MATH_BLOCK_(\d+)%%/g, (_, idx) => {
            return mathBlocks[parseInt(idx)];
        });

        // Sanitize HTML to prevent XSS
        return DOMPurify.sanitize(html, {
            ALLOWED_TAGS: [
                'p', 'br', 'strong', 'em', 'code', 'pre', 'blockquote',
                'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
                'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
                'hr', 'del', 'details', 'summary', 'span', 'div', 'sup', 'sub',
                'dl', 'dt', 'dd', 'kbd', 'var', 'samp', 'abbr', 'mark',
            ],
            ALLOWED_ATTR: [
                'href', 'src', 'alt', 'title', 'class',
                'target', 'rel', 'open', 'colspan', 'rowspan',
            ],
            ALLOW_DATA_ATTR: false,
        });
    }

    function renderMath(el) {
        if (typeof renderMathInElement === 'function') {
            try {
                renderMathInElement(el, {
                    delimiters: [
                        { left: '$$', right: '$$', display: true },
                        { left: '\\[', right: '\\]', display: true },
                        { left: '$', right: '$', display: false },
                        { left: '\\(', right: '\\)', display: false },
                    ],
                    throwOnError: false,
                    ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
                });
            } catch (e) {
                // silently ignore render errors
            }
        }
    }

    function highlightCode(el) {
        el.querySelectorAll('pre code').forEach(block => {
            hljs.highlightElement(block);
        });
    }

    function addCodeCopyButtons(el) {
        el.querySelectorAll('pre').forEach(pre => {
            const code = pre.querySelector('code');
            if (!code) return;

            const lang = (code.className.match(/language-(\w+)/) || [])[1] || '';
            const header = document.createElement('div');
            header.className = 'code-header';

            const langSpan = document.createElement('span');
            langSpan.textContent = lang;
            header.appendChild(langSpan);

            const copyBtn = document.createElement('button');
            copyBtn.className = 'btn-copy-code';
            copyBtn.textContent = 'Copy';
            copyBtn.addEventListener('click', () => {
                navigator.clipboard.writeText(code.textContent).then(() => {
                    copyBtn.textContent = 'Copied!';
                    setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000);
                });
            });
            header.appendChild(copyBtn);

            pre.insertBefore(header, code);
            hljs.highlightElement(code);
        });
    }

    function addMessageActions(msgEl, role, content, msgData, options) {
        const isLast = options && options.isLast;
        const hasFileChangesAfter = options && options.hasFileChangesAfter;
        const actions = document.createElement('div');
        actions.className = 'message-actions';

        if (role === 'assistant') {
            const copyTextBtn = document.createElement('button');
            copyTextBtn.className = 'btn-action-icon';
            copyTextBtn.title = 'Copy as text';
            copyTextBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>';
            copyTextBtn.addEventListener('click', () => {
                const contentEl = msgEl.querySelector('.message-content');
                const text = contentEl ? contentEl.innerText : content;
                navigator.clipboard.writeText(text).then(() => {
                    copyTextBtn.title = 'Copied!';
                    setTimeout(() => { copyTextBtn.title = 'Copy as text'; }, 2000);
                });
            });
            actions.appendChild(copyTextBtn);

            const copyMdBtn = document.createElement('button');
            copyMdBtn.className = 'btn-action-icon';
            copyMdBtn.title = 'Copy as markdown';
            copyMdBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 16.5L2 7.5C2 6.67 2.67 6 3.5 6L20.5 6C21.33 6 22 6.67 22 7.5L22 16.5C22 17.33 21.33 18 20.5 18L3.5 18C2.67 18 2 17.33 2 16.5Z"/><path d="M5.5 15L5.5 9L8 9L10 11.5L12 9L14.5 9L14.5 15"/><path d="M5.5 15L8 15L8 12"/><path d="M10 15L10 11.5"/><path d="M12 15L14.5 15"/><path d="M17.5 12L20 9.5M17.5 12L15 9.5M17.5 12L17.5 15"/></svg>';
            copyMdBtn.addEventListener('click', () => {
                navigator.clipboard.writeText(content).then(() => {
                    copyMdBtn.title = 'Copied!';
                    setTimeout(() => { copyMdBtn.title = 'Copy as markdown'; }, 2000);
                });
            });
            actions.appendChild(copyMdBtn);
        }

        if (role === 'user' && msgData) {
            // Edit button
            const editBtn = document.createElement('button');
            editBtn.className = 'btn-action-icon';
            editBtn.title = 'Edit';
            editBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
            editBtn.addEventListener('click', () => startEdit(msgEl, msgData));
            actions.appendChild(editBtn);
        }

        if (msgData) {
            // Rewind button (hidden on last message — rewinding to last is a no-op)
            if (!isLast) {
                const rewindBtn = document.createElement('button');
                rewindBtn.className = 'btn-action-icon';
                rewindBtn.title = 'Rewind to here';
                rewindBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 105.64-11.36L1 10"/></svg>';
                rewindBtn.addEventListener('click', () => openRewindModal(msgEl, msgData.position, !!hasFileChangesAfter));
                actions.appendChild(rewindBtn);
            }

            // Fork button
            const forkBtn = document.createElement('button');
            forkBtn.className = 'btn-action-icon';
            forkBtn.title = 'Fork from here';
            forkBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v1a2 2 0 01-2 2H8a2 2 0 01-2-2V9"/><line x1="12" y1="12" x2="12" y2="15"/></svg>';
            forkBtn.addEventListener('click', () => forkConversation(msgData.position));
            actions.appendChild(forkBtn);
        }

        msgEl.appendChild(actions);
    }

    async function forkConversation(position) {
        const conversationId = App.state.currentConversationId;
        if (!conversationId) return;
        try {
            const newConv = await App.api(`/api/conversations/${conversationId}/fork`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ up_to_position: position }),
            });
            await App.loadConversation(newConv.id);
            await Sidebar.refresh();
        } catch (err) {
            console.error('[debug] fork failed', err.message);
            showToast('Fork failed. Check the console for details.');
        }
    }

    function openRewindModal(msgEl, position, hasFileChanges) {
        _rewindPosition = position;
        _rewindMsgEl = msgEl;
        const undoBtn = document.getElementById('rewind-undo-files');
        const keepBtn = document.getElementById('rewind-keep-files');
        if (hasFileChanges) {
            undoBtn.style.display = '';
            keepBtn.textContent = 'Rewind conversation only';
        } else {
            undoBtn.style.display = 'none';
            keepBtn.textContent = 'Rewind';
        }
        document.getElementById('rewind-modal').style.display = 'flex';
    }

    function closeRewindModal() {
        document.getElementById('rewind-modal').style.display = 'none';
        _rewindPosition = null;
        _rewindMsgEl = null;
    }

    async function executeRewind(undoFiles) {
        const conversationId = App.state.currentConversationId;
        if (!conversationId || _rewindPosition === null || !_rewindMsgEl) return;

        const position = _rewindPosition;
        const msgEl = _rewindMsgEl;
        closeRewindModal();

        try {
            const result = await App.api(`/api/conversations/${conversationId}/rewind`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ to_position: position, undo_files: undoFiles }),
            });

            let sibling = msgEl.nextElementSibling;
            while (sibling) {
                const next = sibling.nextElementSibling;
                sibling.remove();
                sibling = next;
            }

            // Update actions on the now-last message (hide rewind, it's now last)
            const existingActions = msgEl.querySelector('.message-actions');
            if (existingActions) {
                const rewindBtn = existingActions.querySelector('[title="Rewind to here"]');
                if (rewindBtn) rewindBtn.remove();
            }

            // Show feedback
            let summary = `Rewound ${result.deleted_messages} message${result.deleted_messages !== 1 ? 's' : ''}`;
            if (result.reverted_files.length > 0) {
                summary += `, reverted ${result.reverted_files.length} file${result.reverted_files.length !== 1 ? 's' : ''}`;
            }
            if (result.skipped_files.length > 0) {
                summary += `, ${result.skipped_files.length} skipped`;
            }
            showToast(summary);
        } catch (err) {
            console.error('[debug] rewind failed', err.message);
            showToast('Rewind failed. Check the console for details.');
        }
    }

    function showToast(message) {
        let container = document.getElementById('toast-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'toast-container';
            document.body.appendChild(container);
        }
        const toast = document.createElement('div');
        toast.className = 'toast';
        toast.textContent = message;
        container.appendChild(toast);
        setTimeout(() => {
            toast.classList.add('toast-fade-out');
            toast.addEventListener('animationend', () => toast.remove());
        }, 3000);
    }

    function initRewindModal() {
        document.getElementById('rewind-close').addEventListener('click', closeRewindModal);
        document.getElementById('rewind-undo-files').addEventListener('click', () => executeRewind(true));
        document.getElementById('rewind-keep-files').addEventListener('click', () => executeRewind(false));
        document.getElementById('rewind-modal').addEventListener('click', (e) => {
            if (e.target.id === 'rewind-modal') closeRewindModal();
        });
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && document.getElementById('rewind-modal').style.display !== 'none') {
                closeRewindModal();
            }
        });
    }

    function startEdit(msgEl, msgData) {
        const contentDiv = msgEl.querySelector('.message-content');
        const originalContent = msgData.content;
        const actionsDiv = msgEl.querySelector('.message-actions');
        if (actionsDiv) actionsDiv.style.display = 'none';

        const textarea = document.createElement('textarea');
        textarea.className = 'message-edit-textarea';
        textarea.value = originalContent;
        textarea.rows = Math.max(2, originalContent.split('\n').length);

        const btnRow = document.createElement('div');
        btnRow.className = 'message-edit-actions';

        const saveBtn = document.createElement('button');
        saveBtn.className = 'btn-modal-save';
        saveBtn.textContent = 'Save & Regenerate';
        saveBtn.addEventListener('click', () => saveEdit(msgEl, msgData, textarea.value));

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'btn-modal-cancel';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => cancelEdit(msgEl, msgData, contentDiv, actionsDiv));

        btnRow.appendChild(cancelBtn);
        btnRow.appendChild(saveBtn);

        contentDiv.style.display = 'none';
        contentDiv.parentNode.insertBefore(textarea, contentDiv.nextSibling);
        contentDiv.parentNode.insertBefore(btnRow, textarea.nextSibling);
        textarea.focus();
    }

    function cancelEdit(msgEl, msgData, contentDiv, actionsDiv) {
        const textarea = msgEl.querySelector('.message-edit-textarea');
        const btnRow = msgEl.querySelector('.message-edit-actions');
        if (textarea) textarea.remove();
        if (btnRow) btnRow.remove();
        contentDiv.style.display = '';
        if (actionsDiv) actionsDiv.style.display = '';
    }

    async function saveEdit(msgEl, msgData, newContent) {
        newContent = newContent.trim();
        if (!newContent) return;

        const conversationId = App.state.currentConversationId;

        try {
            // 1. Update message content
            await App.api(`/api/conversations/${conversationId}/messages/${msgData.id}`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ content: newContent }),
            });

            // 2. Delete messages after this position
            await App.api(`/api/conversations/${conversationId}/messages?after_position=${msgData.position}`, {
                method: 'DELETE',
            });

            // 3. Remove subsequent message DOM elements
            let sibling = msgEl.nextElementSibling;
            while (sibling) {
                const next = sibling.nextElementSibling;
                sibling.remove();
                sibling = next;
            }

            // 4. Restore edit UI
            const textarea = msgEl.querySelector('.message-edit-textarea');
            const btnRow = msgEl.querySelector('.message-edit-actions');
            const contentDiv = msgEl.querySelector('.message-content');
            if (textarea) textarea.remove();
            if (btnRow) btnRow.remove();
            contentDiv.style.display = '';
            contentDiv.textContent = newContent;

            const actionsDiv = msgEl.querySelector('.message-actions');
            if (actionsDiv) actionsDiv.style.display = '';

            // Update msgData so future edits use new content
            msgData.content = newContent;

            // 5. Regenerate AI response
            const body = JSON.stringify({ message: '', regenerate: true });
            const headers = {
                'Content-Type': 'application/json',
                'X-CSRF-Token': App._getCsrfToken(),
            };
            await streamChatResponse(conversationId, body, headers);

        } catch (err) {
            console.error('[debug] edit failed', err.message);
            showToast('Edit failed. Check the console for details.');
        }
    }

    function _sanitizeId(id) {
        return String(id).replace(/\./g, '-').replace(/[^a-zA-Z0-9\-_]/g, '');
    }

    function _dateLabel(dateStr) {
        const d = new Date(dateStr);
        const now = new Date();
        const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
        const entry = new Date(d.getFullYear(), d.getMonth(), d.getDate());
        const diff = Math.floor((today - entry) / 86400000);
        if (diff === 0) return 'Today';
        if (diff === 1) return 'Yesterday';
        if (diff < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });
        return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
    }

    function _dateKey(dateStr) {
        const d = new Date(dateStr);
        return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
    }

    function _insertDateHeader(container, label) {
        const header = document.createElement('div');
        header.className = 'note-date-header';
        header.setAttribute('data-date', label);
        header.innerHTML = `<span class="note-date-line"></span><span class="note-date-label">${escapeHtml(label)}</span><span class="note-date-line"></span>`;
        container.appendChild(header);
    }

    function _buildNoteEntry(msg) {
        const el = document.createElement('div');
        el.className = 'note-entry';
        el.setAttribute('data-msg-id', msg.id);

        const header = document.createElement('div');
        header.className = 'note-entry-header';

        const ts = document.createElement('span');
        ts.className = 'note-entry-timestamp';
        ts.textContent = App.formatTimestamp(msg.created_at);
        ts.title = new Date(msg.created_at).toLocaleString();
        header.appendChild(ts);

        const actions = document.createElement('div');
        actions.className = 'note-entry-actions';

        const editBtn = document.createElement('button');
        editBtn.className = 'note-action-btn';
        editBtn.title = 'Edit entry';
        editBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
        editBtn.addEventListener('click', () => _startNoteEntryEdit(el, msg));
        actions.appendChild(editBtn);

        const deleteBtn = document.createElement('button');
        deleteBtn.className = 'note-action-btn note-action-delete';
        deleteBtn.title = 'Delete entry';
        deleteBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
        deleteBtn.addEventListener('click', () => _confirmDeleteNoteEntry(el, msg));
        actions.appendChild(deleteBtn);

        header.appendChild(actions);
        el.appendChild(header);

        const content = document.createElement('div');
        content.className = 'note-entry-content';
        content.innerHTML = renderMarkdown(msg.content);
        renderMath(content);
        addCodeCopyButtons(content);
        el.appendChild(content);

        return el;
    }

    function _startNoteEntryEdit(el, msg) {
        const contentDiv = el.querySelector('.note-entry-content');
        const actionsDiv = el.querySelector('.note-entry-actions');
        if (actionsDiv) actionsDiv.style.display = 'none';

        const textarea = document.createElement('textarea');
        textarea.className = 'note-edit-textarea';
        textarea.value = msg.content;
        textarea.rows = Math.max(3, msg.content.split('\n').length + 1);

        const btnRow = document.createElement('div');
        btnRow.className = 'note-edit-buttons';

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'btn-modal-cancel';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => {
            textarea.remove();
            btnRow.remove();
            contentDiv.style.display = '';
            if (actionsDiv) actionsDiv.style.display = '';
        });

        const saveBtn = document.createElement('button');
        saveBtn.className = 'btn-modal-save';
        saveBtn.textContent = 'Save';
        saveBtn.addEventListener('click', async () => {
            const newContent = textarea.value.trim();
            if (!newContent) return;
            saveBtn.disabled = true;
            saveBtn.textContent = 'Saving...';
            try {
                await App.api(`/api/conversations/${App.state.currentConversationId}/messages/${msg.id}`, {
                    method: 'PATCH',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ content: newContent }),
                });
                msg.content = newContent;
                textarea.remove();
                btnRow.remove();
                contentDiv.innerHTML = renderMarkdown(newContent);
                renderMath(contentDiv);
                addCodeCopyButtons(contentDiv);
                contentDiv.style.display = '';
                if (actionsDiv) actionsDiv.style.display = '';
            } catch (err) {
                showToast('Failed to save: ' + err.message);
                saveBtn.disabled = false;
                saveBtn.textContent = 'Save';
            }
        });

        btnRow.appendChild(cancelBtn);
        btnRow.appendChild(saveBtn);

        contentDiv.style.display = 'none';
        contentDiv.parentNode.insertBefore(textarea, contentDiv.nextSibling);
        contentDiv.parentNode.insertBefore(btnRow, textarea.nextSibling);
        textarea.focus();
        textarea.setSelectionRange(textarea.value.length, textarea.value.length);
    }

    function _confirmDeleteNoteEntry(el, msg) {
        const existing = el.querySelector('.note-delete-confirm');
        if (existing) return;

        const bar = document.createElement('div');
        bar.className = 'note-delete-confirm';
        bar.innerHTML = '<span>Delete this entry?</span>';

        const yesBtn = document.createElement('button');
        yesBtn.className = 'note-delete-yes';
        yesBtn.textContent = 'Delete';
        yesBtn.addEventListener('click', async () => {
            yesBtn.disabled = true;
            yesBtn.textContent = 'Deleting...';
            try {
                await App.api(`/api/conversations/${App.state.currentConversationId}/messages/${msg.id}`, {
                    method: 'DELETE',
                });
                el.classList.add('note-entry-removing');
                el.addEventListener('animationend', () => el.remove());
            } catch (err) {
                showToast('Failed to delete: ' + err.message);
                bar.remove();
            }
        });

        const noBtn = document.createElement('button');
        noBtn.className = 'note-delete-no';
        noBtn.textContent = 'Cancel';
        noBtn.addEventListener('click', () => bar.remove());

        bar.appendChild(noBtn);
        bar.appendChild(yesBtn);
        el.appendChild(bar);
    }

    function _getWelcomeLogoHtml(size = 64) {
        return `
            <div class="welcome-logo">
                <svg width="${size}" height="${size}" viewBox="0 0 100 100">
                    <path d="M10,80 L50,20 L90,80" stroke="var(--accent)" stroke-width="10" fill="none" />
                    <rect x="42" y="65" width="16" height="16" fill="var(--accent-blue)" />
                </svg>
            </div>
        `;
    }

    function _appendNoteEntry(msg) {
        const container = document.getElementById('messages-container');
        const welcome = document.getElementById('welcome-message');
        if (welcome) welcome.style.display = 'none';

        // Check if we need a date header
        const dateKey = _dateKey(msg.created_at);
        const lastEntry = container.querySelector('.note-entry:last-of-type');
        let needsHeader = true;
        if (lastEntry) {
            const lastMsgDate = lastEntry.getAttribute('data-date-key');
            if (lastMsgDate === dateKey) needsHeader = false;
        }
        if (needsHeader) {
            _insertDateHeader(container, _dateLabel(msg.created_at));
        }

        const el = _buildNoteEntry(msg);
        el.setAttribute('data-date-key', dateKey);
        container.appendChild(el);
    }

    function _loadNoteEntries(messages) {
        const container = document.getElementById('messages-container');
        container.innerHTML = '';
        const welcome = document.getElementById('welcome-message');

        if (messages.length === 0) {
            if (!welcome) {
                const w = document.createElement('div');
                w.id = 'welcome-message';
                w.className = 'welcome-message';
                w.innerHTML = `${_getWelcomeLogoHtml()}<h2>New Note</h2><p>Start adding entries below.</p>`;
                container.appendChild(w);
            } else {
                welcome.innerHTML = `${_getWelcomeLogoHtml()}<h2>New Note</h2><p>Start adding entries below.</p>`;
                welcome.style.display = '';
                container.appendChild(welcome);
            }
            return;
        }

        let lastDateKey = null;
        messages.forEach(msg => {
            const dateKey = _dateKey(msg.created_at);
            if (dateKey !== lastDateKey) {
                _insertDateHeader(container, _dateLabel(msg.created_at));
                lastDateKey = dateKey;
            }
            const el = _buildNoteEntry(msg);
            el.setAttribute('data-date-key', dateKey);
            container.appendChild(el);
        });
        scrollToBottom();
    }

    let _docEditMode = false;
    let _docRawContent = '';

    function _loadDocumentView(messages) {
        const container = document.getElementById('messages-container');
        container.innerHTML = '';
        _docEditMode = false;
        const welcome = document.getElementById('welcome-message');

        if (messages.length === 0) {
            if (!welcome) {
                const w = document.createElement('div');
                w.id = 'welcome-message';
                w.className = 'welcome-message';
                w.innerHTML = `${_getWelcomeLogoHtml()}<h2>New Document</h2><p>Add content below.</p>`;
                container.appendChild(w);
            } else {
                welcome.innerHTML = `${_getWelcomeLogoHtml()}<h2>New Document</h2><p>Add content below.</p>`;
                welcome.style.display = '';
                container.appendChild(welcome);
            }
            return;
        }

        _docRawContent = messages.map(m => m.content).join('\n\n');

        // Document toolbar
        const toolbar = document.createElement('div');
        toolbar.className = 'document-toolbar';
        toolbar.id = 'document-toolbar';

        const wordCount = document.createElement('span');
        wordCount.className = 'document-word-count';
        wordCount.id = 'document-word-count';
        wordCount.textContent = _countWords(_docRawContent);
        toolbar.appendChild(wordCount);

        const editBtn = document.createElement('button');
        editBtn.className = 'document-edit-btn';
        editBtn.id = 'document-edit-btn';
        editBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg><span>Edit</span>';
        editBtn.addEventListener('click', () => _enterDocEditMode());
        toolbar.appendChild(editBtn);

        container.appendChild(toolbar);

        // Rendered view
        const docEl = document.createElement('div');
        docEl.className = 'document-view';
        docEl.id = 'document-view';
        messages.forEach(msg => {
            const section = document.createElement('div');
            section.className = 'document-section';
            section.innerHTML = renderMarkdown(msg.content);
            renderMath(section);
            addCodeCopyButtons(section);
            docEl.appendChild(section);
        });
        container.appendChild(docEl);

        // Hidden edit area (shown on toggle)
        const editArea = document.createElement('div');
        editArea.className = 'document-edit-area';
        editArea.id = 'document-edit-area';
        editArea.style.display = 'none';

        const textarea = document.createElement('textarea');
        textarea.className = 'document-edit-textarea';
        textarea.id = 'document-edit-textarea';
        textarea.value = _docRawContent;
        editArea.appendChild(textarea);

        const btnBar = document.createElement('div');
        btnBar.className = 'document-edit-buttons';

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'btn-modal-cancel';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => _exitDocEditMode(false));

        const saveBtn = document.createElement('button');
        saveBtn.className = 'btn-modal-save';
        saveBtn.id = 'document-save-btn';
        saveBtn.textContent = 'Save';
        saveBtn.addEventListener('click', () => _saveDocument());

        btnBar.appendChild(cancelBtn);
        btnBar.appendChild(saveBtn);
        editArea.appendChild(btnBar);

        container.appendChild(editArea);
    }

    function _countWords(text) {
        const words = text.trim().split(/\s+/).filter(w => w.length > 0);
        const chars = text.length;
        return `${words.length} word${words.length !== 1 ? 's' : ''} \u00b7 ${chars.toLocaleString()} char${chars !== 1 ? 's' : ''}`;
    }

    function _enterDocEditMode() {
        _docEditMode = true;
        const view = document.getElementById('document-view');
        const editArea = document.getElementById('document-edit-area');
        const editBtn = document.getElementById('document-edit-btn');
        const textarea = document.getElementById('document-edit-textarea');

        if (view) view.style.display = 'none';
        if (editArea) editArea.style.display = '';
        if (editBtn) editBtn.style.display = 'none';
        if (textarea) {
            textarea.value = _docRawContent;
            textarea.focus();
            textarea.setSelectionRange(0, 0);
        }
    }

    function _exitDocEditMode(reload) {
        _docEditMode = false;
        const view = document.getElementById('document-view');
        const editArea = document.getElementById('document-edit-area');
        const editBtn = document.getElementById('document-edit-btn');

        if (view) view.style.display = '';
        if (editArea) editArea.style.display = 'none';
        if (editBtn) editBtn.style.display = '';

        if (reload) {
            // Re-render the view with updated content
            if (view) {
                view.innerHTML = '';
                const section = document.createElement('div');
                section.className = 'document-section';
                section.innerHTML = renderMarkdown(_docRawContent);
                renderMath(section);
                addCodeCopyButtons(section);
                view.appendChild(section);
            }
            const wc = document.getElementById('document-word-count');
            if (wc) wc.textContent = _countWords(_docRawContent);
        }
    }

    async function _saveDocument() {
        const textarea = document.getElementById('document-edit-textarea');
        const saveBtn = document.getElementById('document-save-btn');
        if (!textarea) return;
        const content = textarea.value.trim();
        if (!content) {
            showToast('Document cannot be empty');
            return;
        }

        saveBtn.disabled = true;
        saveBtn.textContent = 'Saving...';

        try {
            await App.api(`/api/conversations/${App.state.currentConversationId}/document`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ content }),
            });
            _docRawContent = content;
            _exitDocEditMode(true);
            showToast('Document saved');
        } catch (err) {
            showToast('Failed to save: ' + err.message);
        } finally {
            saveBtn.disabled = false;
            saveBtn.textContent = 'Save';
        }
    }

    function appendMessage(role, content, msgData) {
        const container = document.getElementById('messages-container');
        const welcome = document.getElementById('welcome-message');
        if (welcome) welcome.style.display = 'none';

        const el = document.createElement('div');
        el.className = `message ${role}`;

        const roleDiv = document.createElement('div');
        roleDiv.className = 'message-role';
        if (role === 'user') {
            const localUserId = (App.state && App.state.localUserId) || null;
            const msgUserId = msgData && msgData.user_id;
            if (msgUserId && localUserId && msgUserId !== localUserId) {
                roleDiv.textContent = (msgData && msgData.user_display_name) || 'TEAMMATE';
                el.classList.add('remote');
            } else {
                roleDiv.textContent = 'YOU';
            }
        } else if (role === 'assistant') {
            roleDiv.textContent = 'ANTEROOM';
        } else {
            el.classList.add('system-hidden');
            roleDiv.textContent = 'CONTEXT';
        }
        el.appendChild(roleDiv);

        const contentDiv = document.createElement('div');
        contentDiv.className = 'message-content';
        if (role === 'user') {
            contentDiv.textContent = content;
        } else {
            contentDiv.innerHTML = renderMarkdown(content);
        }
        el.appendChild(contentDiv);

        if (role === 'user') {
            const files = Attachments.getFiles();
            if (files.length > 0) {
                const attDiv = document.createElement('div');
                attDiv.className = 'message-attachments';
                files.forEach(f => {
                    const chip = document.createElement('div');
                    chip.className = 'attachment-chip';
                    if (f.type.startsWith('image/')) {
                        const img = document.createElement('img');
                        img.src = URL.createObjectURL(f);
                        chip.appendChild(img);
                    }
                    chip.appendChild(document.createTextNode(f.name));
                    attDiv.appendChild(chip);
                });
                el.appendChild(attDiv);
            }
        }

        container.appendChild(el);
        scrollToBottom();
        return el;
    }

    function _ensureThinkingElement() {
        if (document.getElementById('thinking')) return;
        const el = document.createElement('div');
        el.className = 'thinking-indicator';
        el.id = 'thinking';
        el.innerHTML = '<span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>'
            + '<span class="thinking-phase-label" id="thinking-phase"></span>';
        if (currentAssistantEl) {
            currentAssistantEl.appendChild(el);
        } else {
            const container = document.getElementById('messages-container');
            container.appendChild(el);
        }
        scrollToBottom();
    }

    function _startWatchdog() {
        _clearWatchdog();
        _streamWatchdog = setTimeout(() => {
            _streamWatchdog = null;
            if (!App.state.isStreaming) return;
            const elapsed = ((Date.now() - _phaseStartTime) / 1000).toFixed(0);
            if (typeof App._debugLog === 'function') {
                App._debugLog('watchdog', 'Stream watchdog fired after ' + elapsed + 's — no server response');
            }
            abortStream();
            hideThinking();
            setStreaming(false);
            _pendingUserMessages = [];
            showToast('No response from server after ' + elapsed + 's. Check your connection and try again.');
        }, _WATCHDOG_TIMEOUT_MS);
    }

    function _clearWatchdog() {
        if (_streamWatchdog) {
            clearTimeout(_streamWatchdog);
            _streamWatchdog = null;
        }
    }

    function _resetWatchdog() {
        if (App.state.isStreaming) _startWatchdog();
    }

    function showThinking() {
        if (document.getElementById('thinking')) return;
        _phaseStartTime = Date.now();
        _lastChunkTime = Date.now();
        _streamingChars = 0;
        _thinkingPhase = '';
        _ensureThinkingElement();
        _startPhaseElapsedTimer();
        _startWatchdog();
    }

    function hideThinking() {
        _clearWatchdog();
        document.querySelectorAll('.thinking-indicator').forEach(el => el.remove());
        _thinkingPhase = '';
        _streamingChars = 0;
        _phaseStartTime = 0;
        _lastChunkTime = 0;
        if (_phaseElapsedInterval) {
            clearInterval(_phaseElapsedInterval);
            _phaseElapsedInterval = null;
        }
        if (_stallCheckInterval) {
            clearInterval(_stallCheckInterval);
            _stallCheckInterval = null;
        }
    }

    function _hideThinkingKeepPhase() {
        // Remove the dots UI but keep phase tracking alive for stall detection
        document.querySelectorAll('.thinking-indicator').forEach(el => el.remove());
    }

    function _formatElapsed(ms) {
        const s = Math.floor(ms / 1000);
        return s + 's';
    }

    function _setPhaseLabel(text, isStall) {
        const label = document.getElementById('thinking-phase');
        if (!label) return;
        label.textContent = text;
        if (isStall) {
            label.classList.add('stall');
        } else {
            label.classList.remove('stall');
        }
    }

    function _startPhaseElapsedTimer() {
        if (_phaseElapsedInterval) clearInterval(_phaseElapsedInterval);
        if (_stallCheckInterval) clearInterval(_stallCheckInterval);

        _phaseElapsedInterval = setInterval(() => {
            _renderPhaseLabel();
        }, 500);

        _stallCheckInterval = setInterval(() => {
            if (_thinkingPhase === 'streaming' && _lastChunkTime > 0) {
                const gap = Date.now() - _lastChunkTime;
                if (gap >= _STALL_THRESHOLD_MS) {
                    _ensureThinkingElement();
                    _renderPhaseLabel();
                }
            }
        }, 1000);
    }

    function _renderPhaseLabel() {
        const phaseAge = Date.now() - _phaseStartTime;
        const elapsed = phaseAge >= _PHASE_ELAPSED_DELAY_MS ? ' (' + _formatElapsed(phaseAge) + ')' : '';

        switch (_thinkingPhase) {
            case 'connecting':
                _setPhaseLabel('connecting\u2026' + elapsed, false);
                break;
            case 'waiting':
                _setPhaseLabel('waiting for first token\u2026' + elapsed, false);
                break;
            case 'streaming': {
                const chars = _streamingChars.toLocaleString();
                const gap = Date.now() - _lastChunkTime;
                if (gap >= _STALL_THRESHOLD_MS) {
                    const stallSecs = _formatElapsed(gap);
                    _setPhaseLabel('streaming \u00b7 ' + chars + ' chars \u00b7 stalled ' + stallSecs, true);
                } else {
                    _setPhaseLabel('streaming \u00b7 ' + chars + ' chars', false);
                }
                break;
            }
            case 'retrying':
                // Rendered directly by updateThinkingPhase with retry data
                break;
            default:
                if (!_thinkingPhase) _setPhaseLabel('', false);
                break;
        }
    }

    function updateThinkingPhase(phase, data) {
        _ensureThinkingElement();
        _thinkingPhase = phase;
        _phaseStartTime = Date.now();

        if (phase === 'retrying' && data) {
            const label = 'retry ' + data.attempt + '/' + data.max_attempts;
            _setPhaseLabel(label, true);
        } else {
            _renderPhaseLabel();
        }
    }

    function _onStreamingToken(contentLength) {
        _streamingChars += contentLength;
        _lastChunkTime = Date.now();
        if (_thinkingPhase !== 'streaming') {
            _thinkingPhase = 'streaming';
            _phaseStartTime = Date.now();
        }
    }

    function showError(msgEl, message) {
        const errDiv = document.createElement('div');
        errDiv.className = 'error-message';

        const errText = document.createElement('span');
        errText.textContent = `Error: ${message}`;
        errDiv.appendChild(errText);

        const retryBtn = document.createElement('button');
        retryBtn.className = 'btn-retry';
        retryBtn.textContent = 'Retry';
        retryBtn.addEventListener('click', () => {
            errDiv.remove();
            if (_lastSentText) {
                const body = JSON.stringify({ message: _lastSentText });
                const headers = {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': App._getCsrfToken(),
                };
                streamChatResponse(App.state.currentConversationId, body, headers);
            }
        });
        errDiv.appendChild(retryBtn);

        if (msgEl) {
            msgEl.appendChild(errDiv);
        } else {
            document.getElementById('messages-container').appendChild(errDiv);
        }
        scrollToBottom();
    }

    // Tool call dedup state for web UI
    let _webDedupToolName = '';
    let _webDedupGroup = null;  // the <details> wrapper for grouped calls
    let _webDedupCount = 0;

    function _webDedupFlush() {
        _webDedupToolName = '';
        _webDedupGroup = null;
        _webDedupCount = 0;
    }

    function _webDedupUpdateSummary() {
        if (!_webDedupGroup || _webDedupCount < 2) return;
        const summary = _webDedupGroup.querySelector(':scope > summary');
        if (summary) {
            summary.textContent = `${_webDedupToolName} \u00d7 ${_webDedupCount} `;
        }
    }

    function renderToolCallStart(data) {
        if (!currentAssistantEl) {
            currentAssistantEl = appendMessage('assistant', '');
        }
        const contentEl = currentAssistantEl.querySelector('.message-content');
        const isSubagent = data.tool_name === 'run_agent';
        const details = document.createElement('details');
        details.className = isSubagent ? 'tool-call tool-call-subagent subagent-running' : 'tool-call';
        details.id = `tool-${_sanitizeId(data.id)}`;

        if (isSubagent) {
            _webDedupFlush();
            details.open = true;

            const summary = document.createElement('summary');
            summary.textContent = 'Sub-agent running\u2026 ';
            const spinner = document.createElement('span');
            spinner.className = 'tool-spinner';
            summary.appendChild(spinner);
            const elapsed = document.createElement('span');
            elapsed.className = 'tool-elapsed';
            summary.appendChild(elapsed);
            details.appendChild(summary);
            const startTime = Date.now();
            details._toolTimer = setInterval(() => {
                const secs = Math.floor((Date.now() - startTime) / 1000);
                elapsed.textContent = ` ${secs}s`;
            }, 500);

            const toolContent = document.createElement('div');
            toolContent.className = 'tool-content';
            const promptText = (data.input && data.input.prompt) || '';
            if (promptText) {
                const promptEl = document.createElement('div');
                promptEl.className = 'subagent-loading-prompt';
                promptEl.textContent = promptText;
                toolContent.appendChild(promptEl);
            }
            const cardsContainer = document.createElement('div');
            cardsContainer.className = 'subagent-cards-container';
            toolContent.appendChild(cardsContainer);
            details.appendChild(toolContent);

            contentEl.appendChild(details);
        } else {
            const summary = document.createElement('summary');
            summary.textContent = `Tool: ${data.tool_name} `;
            const spinner = document.createElement('span');
            spinner.className = 'tool-spinner';
            summary.appendChild(spinner);
            const elapsed = document.createElement('span');
            elapsed.className = 'tool-elapsed';
            summary.appendChild(elapsed);
            details.appendChild(summary);
            const startTime = Date.now();
            details._toolTimer = setInterval(() => {
                const secs = Math.floor((Date.now() - startTime) / 1000);
                elapsed.textContent = ` ${secs}s`;
            }, 500);

            const toolContent = document.createElement('div');
            toolContent.className = 'tool-content';
            const inputLabel = document.createElement('strong');
            inputLabel.textContent = 'Input:';
            toolContent.appendChild(inputLabel);
            const inputPre = document.createElement('pre');
            const inputCode = document.createElement('code');
            inputCode.className = 'language-json';
            inputCode.textContent = JSON.stringify(data.input, null, 2);
            inputPre.appendChild(inputCode);
            hljs.highlightElement(inputCode);
            toolContent.appendChild(inputPre);

            details.appendChild(toolContent);

            // Dedup grouping: consecutive same-type tool calls
            const key = data.tool_name || '';
            if (key && key === _webDedupToolName && _webDedupCount >= 1) {
                // Same tool type — add to existing group
                if (_webDedupCount === 1 && !_webDedupGroup) {
                    // Create group wrapper, move the previous tool call into it
                    const group = document.createElement('details');
                    group.className = 'tool-call-group';
                    const groupSummary = document.createElement('summary');
                    groupSummary.textContent = `${key} \u00d7 2 `;
                    group.appendChild(groupSummary);

                    // Find the previous tool call element and wrap it
                    const prevTools = contentEl.querySelectorAll(':scope > .tool-call:not(.tool-call-subagent)');
                    const prevTool = prevTools[prevTools.length - 1];
                    if (prevTool) {
                        contentEl.insertBefore(group, prevTool);
                        group.appendChild(prevTool);
                    }
                    group.appendChild(details);
                    _webDedupGroup = group;
                } else if (_webDedupGroup) {
                    // Group already exists — append
                    _webDedupGroup.appendChild(details);
                }
                _webDedupCount++;
                _webDedupUpdateSummary();
            } else {
                // Different tool type — flush and start new tracking
                _webDedupFlush();
                _webDedupToolName = key;
                _webDedupCount = 1;
                contentEl.appendChild(details);
            }
        }

        scrollToBottom();
    }

    function _clearAllToolTimers() {
        document.querySelectorAll('details[id^="tool-"]').forEach(details => {
            if (details._toolTimer) {
                clearInterval(details._toolTimer);
                details._toolTimer = null;
            }
        });
    }

    function renderToolCallEnd(data) {
        const details = document.getElementById(`tool-${_sanitizeId(data.id)}`);
        if (!details) return;
        if (details._toolTimer) {
            clearInterval(details._toolTimer);
            details._toolTimer = null;
        }
        const spinner = details.querySelector('.tool-spinner');
        if (spinner) spinner.remove();

        const isSubagent = details.classList.contains('tool-call-subagent');

        const summary = details.querySelector('summary');
        if (summary) {
            const statusClass = data.status === 'success' ? 'tool-status-success' : 'tool-status-error';
            details.classList.add(statusClass);
        }

        if (isSubagent) {
            details.classList.remove('subagent-running');
            if (summary) {
                summary.textContent = data.status === 'success' ? 'Sub-agent complete' : 'Sub-agent failed';
            }
            // Hide loading prompt if still visible (no subagent_start events arrived)
            const loadingPrompt = details.querySelector('.subagent-loading-prompt');
            if (loadingPrompt) loadingPrompt.style.display = 'none';
        } else {
            const toolContent = details.querySelector('.tool-content');
            const outputLabel = document.createElement('strong');
            outputLabel.textContent = `Output (${data.status}):`;
            toolContent.appendChild(outputLabel);
            const outputPre = document.createElement('pre');
            const outputCode = document.createElement('code');
            outputCode.className = 'language-json';
            outputCode.textContent = JSON.stringify(data.output, null, 2);
            outputPre.appendChild(outputCode);
            hljs.highlightElement(outputCode);
            toolContent.appendChild(outputPre);
        }
    }

    function renderSubagentEvent(data) {
        if (!currentAssistantEl) return;
        const contentEl = currentAssistantEl.querySelector('.message-content');
        const kind = data.kind;
        const agentId = data.agent_id;

        // Find the last tool-call-subagent container (handles concurrent run_agent calls)
        const allSubagentCalls = contentEl.querySelectorAll('.tool-call-subagent');
        const parentToolCall = allSubagentCalls.length > 0 ? allSubagentCalls[allSubagentCalls.length - 1] : null;
        const cardsContainer = parentToolCall
            ? parentToolCall.querySelector('.subagent-cards-container')
            : null;
        const insertTarget = cardsContainer || contentEl;

        if (kind === 'subagent_start') {
            // Hide the loading prompt once real cards start arriving
            if (parentToolCall) {
                const loadingPrompt = parentToolCall.querySelector('.subagent-loading-prompt');
                if (loadingPrompt) loadingPrompt.style.display = 'none';
            }

            const card = document.createElement('div');
            card.className = 'subagent-card';
            card.id = `subagent-${_sanitizeId(agentId)}`;

            const header = document.createElement('div');
            header.className = 'subagent-header';
            const label = document.createElement('span');
            label.className = 'subagent-label';
            label.textContent = agentId;
            const model = document.createElement('span');
            model.className = 'subagent-model';
            model.textContent = data.model || '';
            header.appendChild(label);
            header.appendChild(model);

            const prompt = document.createElement('div');
            prompt.className = 'subagent-prompt';
            prompt.textContent = data.prompt || '';

            const tools = document.createElement('div');
            tools.className = 'subagent-tools';

            card.appendChild(header);
            card.appendChild(prompt);
            card.appendChild(tools);
            insertTarget.appendChild(card);
            scrollToBottom();
        } else if (kind === 'tool_call_start' && agentId) {
            const card = document.getElementById(`subagent-${_sanitizeId(agentId)}`);
            if (card) {
                const tools = card.querySelector('.subagent-tools');
                if (tools) {
                    const chip = document.createElement('span');
                    chip.className = 'subagent-tool-chip';
                    chip.textContent = data.tool_name || 'tool';
                    tools.appendChild(chip);
                }
            }
        } else if (kind === 'subagent_end') {
            const card = document.getElementById(`subagent-${_sanitizeId(agentId)}`);
            if (card) {
                card.classList.add(data.error ? 'subagent-error' : 'subagent-done');
                const footer = document.createElement('div');
                footer.className = 'subagent-footer';
                const elapsed = data.elapsed_seconds != null ? `${data.elapsed_seconds.toFixed(1)}s` : '';
                const toolCount = (data.tool_calls || []).length;
                const errorMsg = data.error ? String(data.error).slice(0, 200) : '';
                footer.textContent = errorMsg
                    ? `Failed: ${errorMsg}`
                    : `Done in ${elapsed} · ${toolCount} tool call${toolCount !== 1 ? 's' : ''}`;
                card.appendChild(footer);

                // Clear running animation from parent if all sub-agents are done
                if (parentToolCall && parentToolCall.classList.contains('subagent-running')) {
                    parentToolCall.classList.remove('subagent-running');
                }
            }
        }
    }

    function setStreaming(streaming) {
        App.state.isStreaming = streaming;
        document.getElementById('btn-stop').style.display = streaming ? 'flex' : 'none';
        document.getElementById('btn-send').style.display = streaming ? 'inline-flex' : 'flex';
    }

    async function stopGeneration() {
        if (!App.state.currentConversationId) return;
        // Immediately reset client state so UI is responsive
        abortStream();
        hideThinking();
        _clearAllToolTimers();
        setStreaming(false);
        _pendingUserMessages = [];
        try {
            await App.api(`/api/conversations/${App.state.currentConversationId}/stop`, { method: 'POST' });
        } catch (e) {
            // ignore — server may have already cleaned up
        }
    }

    function loadMessages(messages) {
        // Dispatch to type-specific renderer
        if (_conversationType === 'note') {
            _loadNoteEntries(messages);
            return;
        }
        if (_conversationType === 'document') {
            _loadDocumentView(messages);
            return;
        }

        const container = document.getElementById('messages-container');
        container.innerHTML = '';
        const welcome = document.getElementById('welcome-message');

        if (messages.length === 0) {
            if (!welcome) {
                const w = document.createElement('div');
                w.id = 'welcome-message';
                w.className = 'welcome-message';
                w.innerHTML = `${_getWelcomeLogoHtml()}<h2>Welcome to the Anteroom</h2><p>Your connection is secure. How may I assist you today?</p>`;
                container.appendChild(w);
            } else {
                welcome.innerHTML = `${_getWelcomeLogoHtml()}<h2>Welcome to the Anteroom</h2><p>Your connection is secure. How may I assist you today?</p>`;
                welcome.style.display = '';
                container.appendChild(welcome);
            }
            return;
        }

        messages.forEach(msg => {
            const el = document.createElement('div');
            el.className = `message ${msg.role}`;

            const roleDiv = document.createElement('div');
            roleDiv.className = 'message-role';
            if (msg.role === 'user') {
                const localUserId = (App.state && App.state.localUserId) || null;
                if (msg.user_id && localUserId && msg.user_id !== localUserId) {
                    roleDiv.textContent = msg.user_display_name || 'TEAMMATE';
                    el.classList.add('remote');
                } else {
                    roleDiv.textContent = 'YOU';
                }
            } else if (msg.role === 'assistant') {
                roleDiv.textContent = 'ANTEROOM';
            } else {
                // system role: hide by default (compaction summaries, internal context)
                el.classList.add('system-hidden');
                roleDiv.textContent = 'CONTEXT';
            }
            el.appendChild(roleDiv);

            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            if (msg.role === 'user') {
                contentDiv.textContent = msg.content;
            } else {
                contentDiv.innerHTML = renderMarkdown(msg.content);
            }
            el.appendChild(contentDiv);

            if (msg.role === 'assistant') {
                renderMath(contentDiv);
                addCodeCopyButtons(contentDiv);
            }

            if (msg.attachments && msg.attachments.length > 0) {
                const attDiv = document.createElement('div');
                attDiv.className = 'message-attachments';
                msg.attachments.forEach(att => {
                    const chip = document.createElement('div');
                    chip.className = 'attachment-chip';
                    chip.textContent = att.filename;
                    attDiv.appendChild(chip);
                });
                el.appendChild(attDiv);
            }

            if (msg.tool_calls && msg.tool_calls.length > 0) {
                msg.tool_calls.forEach(tc => {
                    const details = document.createElement('details');
                    const statusClass = tc.status === 'success' ? 'tool-status-success' : 'tool-status-error';
                    details.className = `tool-call ${statusClass}`;

                    const summary = document.createElement('summary');
                    summary.textContent = `Tool: ${tc.tool_name} (${tc.status})`;
                    details.appendChild(summary);

                    const toolContent = document.createElement('div');
                    toolContent.className = 'tool-content';

                    const inputLabel = document.createElement('strong');
                    inputLabel.textContent = 'Input:';
                    toolContent.appendChild(inputLabel);
                    const inputPre = document.createElement('pre');
                    const inputCode = document.createElement('code');
                    inputCode.className = 'language-json';
                    inputCode.textContent = JSON.stringify(tc.input, null, 2);
                    inputPre.appendChild(inputCode);
                    hljs.highlightElement(inputCode);
                    toolContent.appendChild(inputPre);

                    if (tc.output) {
                        const outputLabel = document.createElement('strong');
                        outputLabel.textContent = 'Output:';
                        toolContent.appendChild(outputLabel);
                        const outputPre = document.createElement('pre');
                        const outputCode = document.createElement('code');
                        outputCode.className = 'language-json';
                        outputCode.textContent = JSON.stringify(tc.output, null, 2);
                        outputPre.appendChild(outputCode);
                        hljs.highlightElement(outputCode);
                        toolContent.appendChild(outputPre);
                    }

                    details.appendChild(toolContent);
                    el.querySelector('.message-content').appendChild(details);
                });
            }

            // Add action buttons (copy, fork, edit, rewind)
            const idx = messages.indexOf(msg);
            const isLast = idx === messages.length - 1;
            const hasFileChangesAfter = messages.slice(idx + 1).some(m =>
                m.tool_calls && m.tool_calls.some(tc =>
                    tc.tool_name === 'write_file' || tc.tool_name === 'edit_file'
                )
            );
            addMessageActions(el, msg.role, msg.content, msg, { isLast, hasFileChangesAfter });

            container.appendChild(el);
        });
        scrollToBottom();
    }

    function scrollToBottom() {
        const container = document.getElementById('messages-container');
        container.scrollTop = container.scrollHeight;
    }

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // --- Remote Collaboration ---

    function appendRemoteMessage(role, content) {
        const el = appendMessage(role, content);
        const roleDiv = el.querySelector('.message-role');
        if (roleDiv && role === 'user') {
            roleDiv.textContent = 'TEAMMATE';
            el.classList.add('remote');
        }
        return el;
    }

    function startRemoteStream() {
        _remoteAssistantContent = '';
        _remoteAssistantEl = appendMessage('assistant', '');
        const roleDiv = _remoteAssistantEl.querySelector('.message-role');
        if (roleDiv) {
            roleDiv.textContent = 'ANTEROOM (remote)';
        }
    }

    function handleRemoteToken(content) {
        if (!_remoteAssistantEl) return;
        _remoteAssistantContent += content;
        const contentEl = _remoteAssistantEl.querySelector('.message-content');
        contentEl.innerHTML = renderMarkdown(_remoteAssistantContent);
        renderMath(contentEl);
        highlightCode(contentEl);
        scrollToBottom();
    }

    function finalizeRemoteStream() {
        if (!_remoteAssistantEl) return;
        const contentEl = _remoteAssistantEl.querySelector('.message-content');
        contentEl.innerHTML = renderMarkdown(_remoteAssistantContent);
        renderMath(contentEl);
        addCodeCopyButtons(contentEl);
        _remoteAssistantEl = null;
        _remoteAssistantContent = '';
        scrollToBottom();
    }

    function showApprovalPrompt(data) {
        const container = document.getElementById('messages-container');
        if (!container) return;

        const el = document.createElement('div');
        el.className = 'approval-prompt';
        el.setAttribute('data-approval-id', data.approval_id);

        const icon = document.createElement('div');
        icon.className = 'approval-icon';
        icon.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';

        const body = document.createElement('div');
        body.className = 'approval-body';

        const title = document.createElement('div');
        title.className = 'approval-title';
        title.textContent = 'Approval Required';
        body.appendChild(title);

        const reason = document.createElement('div');
        reason.className = 'approval-reason';
        reason.textContent = data.reason || 'A potentially destructive operation needs your confirmation.';
        body.appendChild(reason);

        if (data.details) {
            const details = document.createElement('div');
            details.className = 'approval-details';
            if (data.details.command) {
                const code = document.createElement('code');
                code.textContent = data.details.command;
                details.appendChild(code);
            } else if (data.details.path) {
                const code = document.createElement('code');
                code.textContent = data.details.path;
                details.appendChild(code);
            }
            body.appendChild(details);
        }

        const actions = document.createElement('div');
        actions.className = 'approval-actions';

        const denyBtn = document.createElement('button');
        denyBtn.className = 'approval-btn approval-deny';
        denyBtn.textContent = 'Deny';
        denyBtn.addEventListener('click', () => _respondApproval(data.approval_id, false, 'once', el));
        actions.appendChild(denyBtn);

        const allowOnceBtn = document.createElement('button');
        allowOnceBtn.className = 'approval-btn approval-allow';
        allowOnceBtn.textContent = 'Allow Once';
        allowOnceBtn.addEventListener('click', () => _respondApproval(data.approval_id, true, 'once', el));
        actions.appendChild(allowOnceBtn);

        const allowSessionBtn = document.createElement('button');
        allowSessionBtn.className = 'approval-btn approval-session';
        allowSessionBtn.textContent = 'Allow Session';
        allowSessionBtn.addEventListener('click', () => _respondApproval(data.approval_id, true, 'session', el));
        actions.appendChild(allowSessionBtn);

        const allowAlwaysBtn = document.createElement('button');
        allowAlwaysBtn.className = 'approval-btn approval-always';
        allowAlwaysBtn.textContent = 'Allow Always';
        allowAlwaysBtn.addEventListener('click', () => _respondApproval(data.approval_id, true, 'always', el));
        actions.appendChild(allowAlwaysBtn);

        body.appendChild(actions);
        el.appendChild(icon);
        el.appendChild(body);
        container.appendChild(el);
        scrollToBottom();
    }

    async function _respondApproval(approvalId, approved, scope, el) {
        const buttons = el.querySelectorAll('.approval-btn');
        buttons.forEach(b => { b.disabled = true; });

        try {
            await App.api(`/api/approvals/${encodeURIComponent(approvalId)}/respond`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ approved, scope }),
            });
            el.classList.add(approved ? 'approval-allowed' : 'approval-denied');
            const status = document.createElement('div');
            status.className = 'approval-status';
            const scopeLabels = { once: 'Allowed', session: 'Allowed for Session', always: 'Always Allowed' };
            status.textContent = approved ? (scopeLabels[scope] || 'Allowed') : 'Denied';
            const actionsEl = el.querySelector('.approval-actions');
            if (actionsEl) actionsEl.replaceWith(status);
        } catch (err) {
            buttons.forEach(b => { b.disabled = false; });
            showToast('Failed to respond: ' + err.message);
        }
    }

    function resolveApprovalCard(approvalId, approved, reason) {
        const el = document.querySelector(`[data-approval-id="${CSS.escape(approvalId)}"]`);
        if (!el || el.classList.contains('approval-allowed') || el.classList.contains('approval-denied')) return;
        el.classList.add(approved ? 'approval-allowed' : 'approval-denied');
        const status = document.createElement('div');
        status.className = 'approval-status';
        if (reason === 'timed_out') {
            status.textContent = 'Timed out';
        } else {
            status.textContent = approved ? 'Allowed' : 'Denied';
        }
        const actionsEl = el.querySelector('.approval-actions');
        if (actionsEl) actionsEl.replaceWith(status);
    }

    function showAskUserPrompt(data) {
        const container = document.getElementById('messages-container');
        if (!container) return;

        const el = document.createElement('div');
        el.className = 'ask-user-prompt';
        el.setAttribute('data-ask-id', data.ask_id);

        const icon = document.createElement('div');
        icon.className = 'ask-user-icon';
        icon.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';

        const body = document.createElement('div');
        body.className = 'ask-user-body';

        const title = document.createElement('div');
        title.className = 'ask-user-title';
        title.textContent = 'Question';
        body.appendChild(title);

        const question = document.createElement('div');
        question.className = 'ask-user-question';
        question.textContent = data.question;
        body.appendChild(question);

        const actions = document.createElement('div');
        actions.className = 'ask-user-actions';

        if (data.options && data.options.length > 0) {
            data.options.forEach(opt => {
                const btn = document.createElement('button');
                btn.className = 'ask-user-btn ask-user-option';
                btn.textContent = opt;
                btn.addEventListener('click', () => _respondAskUser(data.ask_id, opt, el));
                actions.appendChild(btn);
            });
        } else {
            const inputRow = document.createElement('div');
            inputRow.className = 'ask-user-input-row';
            const input = document.createElement('input');
            input.type = 'text';
            input.className = 'ask-user-input';
            input.placeholder = 'Type your answer...';
            input.maxLength = 4096;
            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && input.value.trim()) {
                    _respondAskUser(data.ask_id, input.value.trim(), el);
                }
            });
            inputRow.appendChild(input);
            const submitBtn = document.createElement('button');
            submitBtn.className = 'ask-user-btn ask-user-submit';
            submitBtn.textContent = 'Submit';
            submitBtn.addEventListener('click', () => {
                if (input.value.trim()) {
                    _respondAskUser(data.ask_id, input.value.trim(), el);
                }
            });
            inputRow.appendChild(submitBtn);
            actions.appendChild(inputRow);
            setTimeout(() => input.focus(), 50);
        }

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'ask-user-btn ask-user-cancel';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => _respondAskUser(data.ask_id, null, el));
        actions.appendChild(cancelBtn);

        body.appendChild(actions);
        el.appendChild(icon);
        el.appendChild(body);
        container.appendChild(el);
        scrollToBottom();
    }

    async function _respondAskUser(askId, answer, el) {
        const buttons = el.querySelectorAll('.ask-user-btn');
        buttons.forEach(b => { b.disabled = true; });
        const input = el.querySelector('.ask-user-input');
        if (input) input.disabled = true;

        const cancelled = answer === null;
        try {
            await App.api(`/api/approvals/${encodeURIComponent(askId)}/respond`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    approved: !cancelled,
                    answer: cancelled ? '' : answer,
                }),
            });
            el.classList.add(cancelled ? 'ask-user-cancelled' : 'ask-user-answered');
            const status = document.createElement('div');
            status.className = 'ask-user-status';
            status.textContent = cancelled ? 'Cancelled' : answer;
            const actionsEl = el.querySelector('.ask-user-actions');
            if (actionsEl) actionsEl.replaceWith(status);
        } catch (err) {
            buttons.forEach(b => { b.disabled = false; });
            if (input) input.disabled = false;
            showToast('Failed to respond: ' + err.message);
        }
    }

    function sendPlanExecution(approvedPlanPrompt) {
        const input = document.getElementById('message-input');
        if (input) {
            input.value = approvedPlanPrompt;
            sendMessage();
        }
    }

    return {
        init, sendMessage, loadMessages, stopGeneration, abortStream, setStreaming, escapeHtml,
        streamChatResponse, isRawMode, setRawMode, setConversationType,
        appendRemoteMessage, startRemoteStream, handleRemoteToken, finalizeRemoteStream,
        showApprovalPrompt, resolveApprovalCard, showAskUserPrompt, showThinkingFromEvent: showThinking,
        renderMarkdown, highlightCode, showToast, sendPlanExecution,
    };
})();
