/**
 * IEUM Web UI - VS Code Style Application
 */

/**
 * i18n translation helper.
 * Looks up a key from the translations loaded by the i18n bootstrap script.
 * Supports {placeholder} substitution via an optional params object.
 *
 * @param {string} key - Translation key (e.g. 'webui.send_tooltip')
 * @param {Object} [params] - Placeholder values
 * @returns {string} Translated string, or the key itself as fallback
 */
function _t(key, params) {
    var dict = window.__ieum_i18n || {};
    var text = dict[key] || key;
    if (params) {
        Object.keys(params).forEach(function(k) {
            text = text.replace(new RegExp('\\{' + k + '\\}', 'g'), params[k]);
        });
    }
    return text;
}

/**
 * Returns the BCP-47 locale tag for date/time formatting.
 * Reads from localStorage preference, falls back to browser language.
 * Maps short locale codes (en, ko) to full BCP-47 tags.
 */
function _getLocaleTag() {
    var lang = (localStorage.getItem('ieum_lang') || navigator.language || 'en').toLowerCase();
    var map = { 'ko': 'ko-KR', 'en': 'en-US', 'ja': 'ja-JP', 'zh': 'zh-CN' };
    return map[lang] || lang;
}

class IeumApp {
    constructor() {
        this.ws = null;
        this.sessionId = null;
        this.isConnected = false;
        this.isStreaming = false;
        this.isThinking = false;
        this.pendingApproval = null;
        this.currentMessageEl = null;
        this.thinkingMessageEl = null;
        this.approvalPolicy = localStorage.getItem('ieum_approval_policy') || 'manual';
        this.autoApprove = this.approvalPolicy === 'full_trusted';
        this._currentPlan = null;
        this._planOriginalTask = null;
        this.lastSeq = 0;

        // WebSocket reconnection (W02)
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 30;
        this.reconnectBaseDelay = 2000;
        this.reconnectMaxDelay = 60000;
        this.reconnectTimer = null;

        // Monaco Editor
        this.editor = null;
        this.monaco = null;
        this.openFiles = new Map(); // path -> { model, viewState, originalContent }
        this.activeFile = null;
        this.modifiedFiles = new Set(); // Track modified files

        // Sessions
        this.sessions = [];

        // Ralph Loop
        this.ralphLoop = null;

        // Context (Cursor-style selection injection)
        this.contexts = []; // Array of { file, startLine, endLine, content }

        // Command suggestions
        this.commandSuggestionVisible = false;
        this.commandSuggestionIndex = 0;
        this.filteredCommands = [];
        this.commands = [
            { cmd: '/help', desc: _t('webui.commands.help'), icon: '❓' },
            { cmd: '/new', desc: _t('webui.commands.new'), icon: '✨' },
            { cmd: '/clear', desc: _t('webui.commands.clear'), icon: '🗑️' },
            { cmd: '/sessions', desc: _t('webui.commands.sessions'), icon: '📋' },
            { cmd: '/model', desc: _t('webui.commands.model'), icon: '🤖' },
            { cmd: '/tokens', desc: _t('webui.commands.tokens'), icon: '📊' },
            { cmd: '/cost', desc: 'Show session cost ($)', icon: '💰' },
            { cmd: '/skills', desc: _t('webui.commands.skills'), icon: '🎯' },
            { cmd: '/skill', desc: _t('webui.commands.skill'), icon: '⚡' },
            { cmd: '/plan', desc: 'Plan mode (plan/show/approve/cancel)', icon: '📋' },
            { cmd: '/approval', desc: 'Set approval policy', icon: '🛡️' },
            { cmd: '/audit', desc: 'Replay audit events', icon: '🧾' },
            { cmd: '/ralph', desc: _t('webui.commands.ralph'), icon: '🔄' },
            { cmd: '/stop', desc: _t('webui.commands.stop'), icon: '🛑' },
            { cmd: '/autocommit', desc: 'Toggle git auto-commit', icon: '📝' },
            { cmd: '/undo', desc: 'Undo last git commit', icon: '↩️' },
            { cmd: '/verify', desc: 'Verify loop (toggle/run/status)', icon: '✅' },
            { cmd: '/checkpoint', desc: 'Checkpoint (save/list/restore)', icon: '💾' },
            { cmd: '/memory', desc: 'Memory (list/show/add/delete)', icon: '🧠' },
            { cmd: '/task', desc: 'Background tasks (submit/list/cancel)', icon: '⚙️' },
        ];

        // Skills cache
        this.skillsCache = null;

        // File suggestions (@)
        this.fileSuggestionVisible = false;
        this.fileSuggestionIndex = 0;
        this.filteredFiles = [];
        this.fileTreeData = [];

        // Hidden files toggle
        this.showHiddenFiles = false;

        // Model settings
        this.modelSettings = {
            provider: 'ollama',
            baseUrl: 'http://localhost:11434',
            apiKey: '',
            model: ''
        };

        this.init();
    }

    async init() {
        this.bindElements();
        this.bindEvents();
        this.applyApprovalPolicyUI();
        this.initKeyboardShortcuts(); // W06

        // Delegated handler for copy-code-btn (avoids inline onclick XSS)
        document.addEventListener('click', (e) => {
            const btn = e.target.closest('.copy-code-btn');
            if (btn) {
                const text = btn.getAttribute('data-copy-text') || '';
                // Decode HTML entities back to plain text for clipboard
                const ta = document.createElement('textarea');
                ta.innerHTML = text;
                navigator.clipboard.writeText(ta.value);
            }
        });
        await this.initMonaco();
        await this.loadSettings();
        await this.loadSessions();
        this.loadFiles();
        this.initResizers();
        this.showWelcomeMessage(); // W07
    }

    bindElements() {
        // Main elements
        this.messagesEl = document.getElementById('messages');
        this.messageInput = document.getElementById('messageInput');
        this.sendBtn = document.getElementById('sendBtn');
        this.sessionList = document.getElementById('sessionList');

        // Panels
        this.explorerPanel = document.getElementById('explorerPanel');
        this.chatPanel = document.getElementById('chatPanel');
        this.fileTree = document.getElementById('fileTree');

        // Monaco
        this.editorContainer = document.getElementById('editorContainer');
        this.monacoEditorEl = document.getElementById('monacoEditor');
        this.welcomeScreen = document.getElementById('welcomeScreen');
        this.editorTabs = document.getElementById('editorTabs');
        this.breadcrumb = document.getElementById('breadcrumb');

        // Status bar
        this.connectionStatus = document.getElementById('connectionStatus');
        this.currentFileEl = document.getElementById('currentFile');
        this.cursorPosition = document.getElementById('cursorPosition');
        this.fileLanguage = document.getElementById('fileLanguage');
        this.modelNameEl = document.getElementById('modelName');

        // Buttons
        this.newSessionBtn = document.getElementById('newSessionBtn');
        this.refreshFilesBtn = document.getElementById('refreshFilesBtn');
        this.toggleSessionsBtn = document.getElementById('toggleSessionsBtn');
        this.toggleHiddenBtn = document.getElementById('toggleHiddenBtn');

        // Settings modal
        this.settingsBtn = document.getElementById('settingsBtn');
        this.settingsModal = document.getElementById('settingsModal');
        this.closeSettingsBtn = document.getElementById('closeSettingsBtn');
        this.saveSettingsBtn = document.getElementById('saveSettingsBtn');
        this.autoApproveToggleBtn = document.getElementById('autoApproveToggleBtn');
        this.autoApproveIndicator = document.getElementById('autoApproveIndicator');

        // Model settings
        this.providerSelect = document.getElementById('providerSelect');
        this.baseUrlInput = document.getElementById('baseUrlInput');
        this.baseUrlGroup = document.getElementById('baseUrlGroup');
        this.apiKeyInput = document.getElementById('apiKeyInput');
        this.apiKeyGroup = document.getElementById('apiKeyGroup');
        this.modelSelect = document.getElementById('modelSelect');
        this.modelInput = document.getElementById('modelInput');
        this.modelSelectWrapper = document.getElementById('modelSelectWrapper');
        this.modelInputWrapper = document.getElementById('modelInputWrapper');
        this.modelStatus = document.getElementById('modelStatus');
        this.refreshModelsBtn = document.getElementById('refreshModelsBtn');

        // Toast
        this.toastContainer = document.getElementById('toastContainer');
    }

    bindEvents() {
        // Send message
        this.sendBtn.addEventListener('click', () => this.sendMessage());
        this.messageInput.addEventListener('keydown', (e) => {
            // Handle command suggestion navigation
            if (this.commandSuggestionVisible) {
                if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    this.navigateCommandSuggestion(1);
                    return;
                } else if (e.key === 'ArrowUp') {
                    e.preventDefault();
                    this.navigateCommandSuggestion(-1);
                    return;
                } else if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    this.selectCommandSuggestion();
                    return;
                } else if (e.key === 'Escape') {
                    this.hideCommandSuggestions();
                    return;
                } else if (e.key === 'Tab') {
                    e.preventDefault();
                    this.selectCommandSuggestion();
                    return;
                }
            }

            // Handle file suggestion navigation
            if (this.fileSuggestionVisible) {
                if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    this.navigateFileSuggestion(1);
                    return;
                } else if (e.key === 'ArrowUp') {
                    e.preventDefault();
                    this.navigateFileSuggestion(-1);
                    return;
                } else if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    this.selectFileSuggestion();
                    return;
                } else if (e.key === 'Escape') {
                    this.hideFileSuggestions();
                    return;
                } else if (e.key === 'Tab') {
                    e.preventDefault();
                    this.selectFileSuggestion();
                    return;
                }
            }

            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                this.sendMessage();
            }
        });

        // Auto-resize textarea and show suggestions
        this.messageInput.addEventListener('input', () => {
            this.messageInput.style.height = 'auto';
            this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 150) + 'px';

            const value = this.messageInput.value;

            // Check for slash command (only at start)
            if (value.startsWith('/') && !value.includes(' ')) {
                this.showCommandSuggestions(value);
                this.hideFileSuggestions();
            }
            // Check for @ file reference
            else if (value.includes('@')) {
                const atIndex = value.lastIndexOf('@');
                const afterAt = value.slice(atIndex + 1);
                // Only show if @ is not followed by space
                if (!afterAt.includes(' ')) {
                    this.showFileSuggestions(value);
                    this.hideCommandSuggestions();
                } else {
                    this.hideFileSuggestions();
                }
            } else {
                this.hideCommandSuggestions();
                this.hideFileSuggestions();
            }
        });

        // Session management
        this.newSessionBtn?.addEventListener('click', () => this.createSession());
        this.toggleSessionsBtn?.addEventListener('click', (e) => {
            e.stopPropagation();
            this.toggleSessionsList();
        });

        // Close session dropdown when clicking outside
        document.addEventListener('click', (e) => {
            const container = document.getElementById('sessionListContainer');
            if (container && !container.classList.contains('hidden')) {
                if (!e.target.closest('#sessionListContainer') && !e.target.closest('#toggleSessionsBtn')) {
                    container.classList.add('hidden');
                }
            }
        });

        // Files
        this.refreshFilesBtn?.addEventListener('click', () => this.loadFiles());
        this.toggleHiddenBtn?.addEventListener('click', () => {
            this.showHiddenFiles = !this.showHiddenFiles;
            this.toggleHiddenBtn.classList.toggle('text-mx-green', this.showHiddenFiles);
            this.toggleHiddenBtn.classList.toggle('text-dark-400', !this.showHiddenFiles);
            this.toggleHiddenBtn.setAttribute('data-tooltip', this.showHiddenFiles ? _t('webui.hide_hidden_tooltip') : _t('webui.show_hidden_tooltip'));
            this.loadFiles();
        });

        // File tree drag and drop (root level)
        this.fileTree?.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        });

        this.fileTree?.addEventListener('drop', async (e) => {
            // Only handle if dropped directly on fileTree, not on a folder
            if (e.target === this.fileTree || e.target.id === 'fileTree') {
                e.preventDefault();
                try {
                    const data = JSON.parse(e.dataTransfer.getData('text/plain'));
                    await this.moveFile(data.path, data.name, '');
                } catch (err) {
                    console.error('Drop error:', err);
                }
            }
        });

        // Editor toolbar
        document.getElementById('newFolderBtn')?.addEventListener('click', () => this.createNewFolder());
        document.getElementById('newFileBtn')?.addEventListener('click', () => this.createNewFile());
        document.getElementById('saveFileBtn')?.addEventListener('click', () => this.saveCurrentFile());
        document.getElementById('saveAllBtn')?.addEventListener('click', () => this.saveAllFiles());
        document.getElementById('undoBtn')?.addEventListener('click', () => this.editor?.trigger('keyboard', 'undo', null));
        document.getElementById('redoBtn')?.addEventListener('click', () => this.editor?.trigger('keyboard', 'redo', null));

        // Settings
        this.settingsBtn?.addEventListener('click', () => this.showSettingsModal());
        this.closeSettingsBtn?.addEventListener('click', () => this.hideSettingsModal());
        this.saveSettingsBtn?.addEventListener('click', () => this.saveSettings());
        this.settingsModal?.addEventListener('click', (e) => {
            if (e.target === this.settingsModal) this.hideSettingsModal();
        });
        this.autoApproveToggleBtn?.addEventListener('click', () => this.toggleAutoApprove());

        // Model settings
        this.providerSelect?.addEventListener('change', () => this.onProviderChange());
        this.refreshModelsBtn?.addEventListener('click', () => this.fetchModels());

        // API Key visibility toggle
        document.getElementById('toggleApiKeyBtn')?.addEventListener('click', () => this.toggleApiKeyVisibility());
    }

    toggleApiKeyVisibility() {
        const input = document.getElementById('apiKeyInput');
        const eyeIcon = document.getElementById('eyeIcon');
        const eyeOffIcon = document.getElementById('eyeOffIcon');

        if (input.type === 'password') {
            input.type = 'text';
            eyeIcon.classList.add('hidden');
            eyeOffIcon.classList.remove('hidden');
        } else {
            input.type = 'password';
            eyeIcon.classList.remove('hidden');
            eyeOffIcon.classList.add('hidden');
        }
    }

    // Monaco Editor
    async initMonaco() {
        const monaco = await window.monacoReady;
        this.monaco = monaco;

        // Configure theme
        monaco.editor.defineTheme('vscode-dark', {
            base: 'vs-dark',
            inherit: true,
            rules: [],
            colors: {
                'editor.background': '#1e1e1e',
                'editor.foreground': '#cccccc',
                'editorLineNumber.foreground': '#858585',
                'editorCursor.foreground': '#aeafad',
            }
        });

        this.editor = monaco.editor.create(this.monacoEditorEl, {
            theme: 'vscode-dark',
            fontSize: 14,
            fontFamily: "'D2Coding', 'JetBrains Mono', 'Fira Code', Consolas, monospace",
            minimap: { enabled: false },
            scrollBeyondLastLine: false,
            automaticLayout: true,
            tabSize: 4,
            wordWrap: 'on',
            lineNumbers: 'on',
            renderWhitespace: 'selection',
            bracketPairColorization: { enabled: true },
        });

        // Cursor position
        this.editor.onDidChangeCursorPosition((e) => {
            this.cursorPosition.textContent = `Ln ${e.position.lineNumber}, Col ${e.position.column}`;
        });

        // Save on Ctrl+S
        this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
            this.saveCurrentFile();
        });

        // Add selection to context on Ctrl+L (Cursor-style)
        this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => {
            this.addSelectionToContext();
        });

        // Add selection to context on Ctrl+Shift+L (alternative)
        this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyL, () => {
            this.addSelectionToContext();
        });
    }

    addSelectionToContext() {
        if (!this.editor || !this.activeFile) return;

        const selection = this.editor.getSelection();
        if (!selection || selection.isEmpty()) {
            this.showToast(_t('webui.select_text_first'), 'warning');
            return;
        }

        const model = this.editor.getModel();
        const selectedText = model.getValueInRange(selection);
        const startLine = selection.startLineNumber;
        const endLine = selection.endLineNumber;

        // Add to contexts
        this.contexts.push({
            file: this.activeFile,
            startLine,
            endLine,
            content: selectedText
        });

        this.renderContexts();
        this.showToast(_t('webui.context_added', {file: this.activeFile.split('/').pop(), start: startLine, end: endLine}), 'success');

        // Focus message input
        this.messageInput.focus();
    }

    renderContexts() {
        let contextContainer = document.getElementById('contextContainer');

        if (this.contexts.length === 0) {
            if (contextContainer) {
                contextContainer.remove();
            }
            return;
        }

        if (!contextContainer) {
            contextContainer = document.createElement('div');
            contextContainer.id = 'contextContainer';
            contextContainer.className = 'px-3 py-2 border-b border-vscode-border bg-vscode-sidebar/50';
            this.messageInput.parentElement.parentElement.insertBefore(contextContainer, this.messageInput.parentElement);
        }

        contextContainer.innerHTML = `
            <div class="flex items-center justify-between mb-1">
                <span class="text-xs text-dark-400">${_t('webui.context_label')}</span>
                <button id="clearAllContexts" class="text-xs text-dark-500 hover:text-mx-red">${_t('webui.context_clear_all')}</button>
            </div>
            <div class="flex flex-wrap gap-1">
                ${this.contexts.map((ctx, i) => {
                    const filename = ctx.file.split('/').pop() || ctx.file.split('\\').pop();
                    return `
                        <div class="context-item inline-flex items-center gap-1 bg-vscode-active border border-vscode-border rounded px-2 py-1 text-xs" data-index="${i}">
                            <span class="text-mx-blue">${this.escapeHtml(filename)}</span>
                            <span class="text-dark-400">:${ctx.startLine}-${ctx.endLine}</span>
                            <button class="context-remove ml-1 text-dark-500 hover:text-mx-red" data-index="${i}">×</button>
                        </div>
                    `;
                }).join('')}
            </div>
        `;

        // Bind remove events
        contextContainer.querySelectorAll('.context-remove').forEach(btn => {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const index = parseInt(btn.dataset.index);
                this.contexts.splice(index, 1);
                this.renderContexts();
            });
        });

        // Bind clear all
        document.getElementById('clearAllContexts')?.addEventListener('click', () => {
            this.contexts = [];
            this.renderContexts();
        });

        // Click on context to show preview
        contextContainer.querySelectorAll('.context-item').forEach(item => {
            item.addEventListener('click', () => {
                const index = parseInt(item.dataset.index);
                const ctx = this.contexts[index];
                this.showContextPreview(ctx);
            });
        });
    }

    showContextPreview(ctx) {
        const filename = ctx.file.split('/').pop() || ctx.file.split('\\').pop();
        const preview = ctx.content.length > 500 ? ctx.content.substring(0, 500) + '...' : ctx.content;

        // Create modal
        const modal = document.createElement('div');
        modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
        modal.innerHTML = `
            <div class="bg-vscode-sidebar border border-vscode-border rounded-lg w-[600px] max-h-[80vh] flex flex-col">
                <div class="flex items-center justify-between p-3 border-b border-vscode-border">
                    <div>
                        <span class="text-mx-blue font-medium">${this.escapeHtml(filename)}</span>
                        <span class="text-dark-400 text-sm ml-2">Lines ${ctx.startLine}-${ctx.endLine}</span>
                    </div>
                    <button class="close-preview text-dark-400 hover:text-white">×</button>
                </div>
                <pre class="p-3 overflow-auto text-sm text-vscode-text font-mono bg-vscode-bg flex-1"><code>${this.escapeHtml(preview)}</code></pre>
            </div>
        `;

        modal.addEventListener('click', (e) => {
            if (e.target === modal) modal.remove();
        });
        modal.querySelector('.close-preview').addEventListener('click', () => modal.remove());

        document.body.appendChild(modal);
    }

    async openFile(path, content = null) {
        const monaco = await window.monacoReady;

        // Save current view state
        if (this.activeFile && this.openFiles.has(this.activeFile)) {
            const current = this.openFiles.get(this.activeFile);
            current.viewState = this.editor.saveViewState();
        }

        // Check if already open
        if (this.openFiles.has(path)) {
            this.activeFile = path;
            const file = this.openFiles.get(path);
            this.editor.setModel(file.model);
            if (file.viewState) {
                this.editor.restoreViewState(file.viewState);
            }
        } else {
            // Fetch content if not provided
            if (content === null) {
                try {
                    const response = await fetch(`/api/sessions/${this.sessionId}/files/content?path=${encodeURIComponent(path)}`);
                    if (!response.ok) throw new Error('Failed to load file');
                    const data = await response.json();
                    content = data.content;
                } catch (error) {
                    this.showToast(_t('webui.file_load_failed', {path: path}), 'error');
                    return;
                }
            }

            // Detect language
            const ext = path.split('.').pop()?.toLowerCase() || '';
            const langMap = {
                'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript',
                'py': 'python', 'rb': 'ruby', 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'h': 'cpp',
                'cs': 'csharp', 'go': 'go', 'rs': 'rust', 'php': 'php', 'swift': 'swift',
                'kt': 'kotlin', 'scala': 'scala', 'r': 'r', 'sql': 'sql',
                'html': 'html', 'css': 'css', 'scss': 'scss', 'less': 'less',
                'json': 'json', 'xml': 'xml', 'yaml': 'yaml', 'yml': 'yaml',
                'md': 'markdown', 'sh': 'shell', 'bash': 'shell', 'ps1': 'powershell',
                'dockerfile': 'dockerfile', 'makefile': 'makefile',
            };
            const language = langMap[ext] || 'plaintext';

            // Create model
            const model = monaco.editor.createModel(content, language);
            this.openFiles.set(path, { model, viewState: null, originalContent: content });
            this.activeFile = path;
            this.editor.setModel(model);

            // Listen for content changes to track modifications
            model.onDidChangeContent(() => {
                const fileData = this.openFiles.get(path);
                if (fileData) {
                    const currentContent = model.getValue();
                    const isModified = currentContent !== fileData.originalContent;
                    if (isModified) {
                        this.modifiedFiles.add(path);
                    } else {
                        this.modifiedFiles.delete(path);
                    }
                    this.updateTabs();
                }
            });
        }

        // Show editor
        this.welcomeScreen.classList.add('hidden');
        this.monacoEditorEl.classList.remove('hidden');

        // Update UI
        this.updateTabs();
        this.updateBreadcrumb(path);
        this.updateStatusBar(path);
    }

    updateTabs() {
        this.editorTabs.innerHTML = '';

        for (const [path, file] of this.openFiles) {
            const filename = path.split('/').pop() || path.split('\\').pop() || path;
            const isActive = path === this.activeFile;
            const isModified = this.modifiedFiles.has(path);
            const ext = filename.split('.').pop()?.toLowerCase() || '';

            const iconColors = {
                'js': 'text-mx-yellow', 'ts': 'text-mx-blue', 'tsx': 'text-mx-blue',
                'py': 'text-mx-green', 'html': 'text-mx-orange', 'css': 'text-mx-blue',
                'json': 'text-mx-yellow', 'md': 'text-vscode-muted', 'jsx': 'text-mx-cyan',
                'vue': 'text-mx-green', 'svelte': 'text-mx-orange', 'rs': 'text-mx-orange',
                'go': 'text-mx-cyan', 'java': 'text-mx-red', 'rb': 'text-mx-red',
            };
            const iconColor = iconColors[ext] || 'text-vscode-muted';

            const tab = document.createElement('div');
            tab.className = `tab-item flex items-center gap-1.5 px-3 h-full cursor-pointer ${isActive ? 'active' : ''}`;
            tab.innerHTML = `
                ${isModified ? '<span class="text-white text-xs">●</span>' : ''}
                <svg class="w-4 h-4 flex-shrink-0 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                </svg>
                <span class="text-[13px] truncate max-w-32 ${isActive ? 'text-white' : 'text-dark-400'}">${this.escapeHtml(filename)}</span>
                <button class="tab-close p-0.5 hover:bg-vscode-active rounded opacity-0 group-hover:opacity-100 hover:opacity-100">
                    <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                    </svg>
                </button>
            `;

            tab.addEventListener('click', (e) => {
                if (!e.target.closest('.tab-close')) {
                    this.openFile(path);
                }
            });

            tab.querySelector('.tab-close').addEventListener('click', (e) => {
                e.stopPropagation();
                this.closeFile(path);
            });

            // Show close button on hover
            tab.addEventListener('mouseenter', () => {
                tab.querySelector('.tab-close').style.opacity = '1';
            });
            tab.addEventListener('mouseleave', () => {
                if (!isActive) {
                    tab.querySelector('.tab-close').style.opacity = '0';
                }
            });

            if (isActive) {
                tab.querySelector('.tab-close').style.opacity = '1';
            }

            this.editorTabs.appendChild(tab);
        }

        // Update open editors list in explorer
        this.updateOpenEditorsList();
    }

    updateOpenEditorsList() {
        const openEditorsList = document.getElementById('openEditorsList');
        if (!openEditorsList) return;

        if (this.openFiles.size === 0) {
            openEditorsList.innerHTML = '';
            return;
        }

        openEditorsList.innerHTML = Array.from(this.openFiles.keys()).map(path => {
            const filename = path.split('/').pop() || path.split('\\').pop() || path;
            const isActive = path === this.activeFile;
            const ext = filename.split('.').pop()?.toLowerCase() || '';

            const iconColors = {
                'js': 'text-mx-yellow', 'ts': 'text-mx-blue', 'py': 'text-mx-green',
                'html': 'text-mx-orange', 'css': 'text-mx-blue', 'json': 'text-mx-yellow',
            };
            const iconColor = iconColors[ext] || 'text-vscode-muted';

            return `
                <div class="open-editor-item flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:bg-vscode-active ${isActive ? 'bg-vscode-active' : ''}" data-path="${this.escapeHtml(path)}">
                    <button class="close-editor p-0.5 hover:bg-vscode-border rounded opacity-0 hover:opacity-100">
                        <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                        </svg>
                    </button>
                    <svg class="w-3.5 h-3.5 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>
                    <span class="text-[13px] truncate ${isActive ? 'text-white' : 'text-dark-300'}">${this.escapeHtml(filename)}</span>
                </div>
            `;
        }).join('');

        // Bind events
        openEditorsList.querySelectorAll('.open-editor-item').forEach(el => {
            el.addEventListener('click', (e) => {
                if (!e.target.closest('.close-editor')) {
                    this.openFile(el.dataset.path);
                }
            });
            el.querySelector('.close-editor')?.addEventListener('click', (e) => {
                e.stopPropagation();
                this.closeFile(el.dataset.path);
            });
            el.addEventListener('mouseenter', () => {
                el.querySelector('.close-editor').style.opacity = '1';
            });
            el.addEventListener('mouseleave', () => {
                el.querySelector('.close-editor').style.opacity = '0';
            });
        });
    }

    closeFile(path) {
        // Check if file is modified and confirm before closing
        if (this.modifiedFiles.has(path)) {
            if (!confirm(_t('webui.confirm_unsaved_changes'))) {
                return;
            }
        }

        const file = this.openFiles.get(path);
        if (file) {
            file.model.dispose();
            this.openFiles.delete(path);
            this.modifiedFiles.delete(path);
        }

        if (this.activeFile === path) {
            const remaining = Array.from(this.openFiles.keys());
            if (remaining.length > 0) {
                this.openFile(remaining[remaining.length - 1]);
            } else {
                this.activeFile = null;
                this.welcomeScreen.classList.remove('hidden');
                this.monacoEditorEl.classList.add('hidden');
                this.breadcrumb.innerHTML = `<span>${_t('webui.select_file')}</span>`;
                this.currentFileEl.textContent = _t('webui.no_file');
                this.fileLanguage.textContent = 'Plain Text';
            }
        }

        this.updateTabs();
    }

    updateBreadcrumb(path) {
        const parts = path.replace(/\\/g, '/').split('/');
        this.breadcrumb.innerHTML = parts.map((part, i) => {
            const isLast = i === parts.length - 1;
            return `<span class="${isLast ? 'text-vscode-text' : ''}">${this.escapeHtml(part)}</span>`;
        }).join('<span class="mx-1">/</span>');
    }

    updateStatusBar(path) {
        const filename = path.split('/').pop() || path.split('\\').pop() || path;
        this.currentFileEl.textContent = filename;

        const ext = path.split('.').pop()?.toLowerCase() || '';
        const langNames = {
            'js': 'JavaScript', 'ts': 'TypeScript', 'py': 'Python', 'java': 'Java',
            'html': 'HTML', 'css': 'CSS', 'json': 'JSON', 'md': 'Markdown',
            'go': 'Go', 'rs': 'Rust', 'cpp': 'C++', 'c': 'C', 'rb': 'Ruby',
        };
        this.fileLanguage.textContent = langNames[ext] || 'Plain Text';
    }

    async saveCurrentFile() {
        if (!this.activeFile) return;

        const content = this.editor.getValue();

        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/content`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    path: this.activeFile,
                    content: content
                })
            });

            if (response.ok) {
                // Update original content and clear modified status
                const fileData = this.openFiles.get(this.activeFile);
                if (fileData) {
                    fileData.originalContent = content;
                }
                this.modifiedFiles.delete(this.activeFile);
                this.updateTabs();
                this.showToast(_t('webui.file_saved'), 'success');
            } else {
                throw new Error('Save failed');
            }
        } catch (error) {
            this.showToast(_t('webui.file_save_failed'), 'error');
        }
    }

    createNewFolder() {
        this.showInlineInput('folder');
    }

    createNewFile() {
        this.showInlineInput('file');
    }

    showInlineInput(type) {
        // Remove any existing inline input
        const existing = this.fileTree.querySelector('.inline-input-container');
        if (existing) existing.remove();

        // Create inline input element
        const container = document.createElement('div');
        container.className = 'inline-input-container flex items-center gap-1 px-2 py-1';
        container.style.paddingLeft = '20px';

        const icon = type === 'folder'
            ? `<svg class="w-4 h-4 text-mx-yellow flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
                   <path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd"/>
                   <path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z"/>
               </svg>`
            : `<svg class="w-4 h-4 text-dark-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                   <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
               </svg>`;

        container.innerHTML = `
            ${icon}
            <input type="text" class="inline-input flex-1 bg-vscode-bg border border-vscode-accent rounded px-1 py-0.5 text-sm focus:outline-none"
                   placeholder="${type === 'folder' ? _t('webui.folder_name_placeholder') : _t('webui.file_name_placeholder')}" autofocus>
        `;

        // Insert at the top of file tree
        this.fileTree.insertBefore(container, this.fileTree.firstChild);

        const input = container.querySelector('input');
        input.focus();

        // Handle input events
        input.addEventListener('keydown', async (e) => {
            if (e.key === 'Enter') {
                const name = input.value.trim();
                if (name) {
                    await this.confirmCreate(type, name);
                }
                container.remove();
            } else if (e.key === 'Escape') {
                container.remove();
            }
        });

        input.addEventListener('blur', () => {
            // Small delay to allow Enter key to be processed
            setTimeout(() => {
                if (container.parentNode) {
                    container.remove();
                }
            }, 150);
        });
    }

    async confirmCreate(type, name, overwrite = false) {
        try {
            if (type === 'folder') {
                const response = await fetch(`/api/sessions/${this.sessionId}/files/folder`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path: name, overwrite })
                });

                if (response.ok) {
                    this.showToast(_t('webui.folder_created'), 'success');
                    this.loadFiles();
                } else if (response.status === 409) {
                    // File/folder already exists
                    this.showOverwriteConfirm(type, name);
                } else {
                    const error = await response.json();
                    throw new Error(error.detail || 'Failed to create folder');
                }
            } else {
                const response = await fetch(`/api/sessions/${this.sessionId}/files/content`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path: name, content: '', overwrite })
                });

                if (response.ok) {
                    this.showToast(_t('webui.file_created'), 'success');
                    this.loadFiles();
                    this.openFile('/' + name);
                } else if (response.status === 409) {
                    // File already exists
                    this.showOverwriteConfirm(type, name);
                } else {
                    const error = await response.json();
                    throw new Error(error.detail || 'Failed to create file');
                }
            }
        } catch (error) {
            this.showToast(error.message || _t('webui.create_failed', {type: type === 'folder' ? _t('webui.type_folder') : _t('webui.type_file')}), 'error');
        }
    }

    showOverwriteConfirm(type, name) {
        const typeText = type === 'folder' ? _t('webui.type_folder') : _t('webui.type_file');
        const modal = document.createElement('div');
        modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
        modal.innerHTML = `
            <div class="bg-vscode-sidebar border border-vscode-border rounded-lg shadow-2xl max-w-md w-full mx-4">
                <div class="p-4 border-b border-vscode-border">
                    <h3 class="font-medium text-white">${_t('webui.overwrite_title', {type: typeText})}</h3>
                </div>
                <div class="p-4">
                    <p class="text-dark-300 text-sm">${_t('webui.overwrite_message', {name: name, type: typeText})}</p>
                    <p class="text-dark-400 text-xs mt-2">${_t('webui.overwrite_confirm')}</p>
                </div>
                <div class="p-4 border-t border-vscode-border flex items-center justify-end gap-3">
                    <button class="cancel-btn px-4 py-2 rounded border border-vscode-border text-dark-300 hover:text-white hover:border-dark-500 transition-colors">
                        ${_t('webui.cancel_btn')}
                    </button>
                    <button class="overwrite-btn px-4 py-2 rounded bg-mx-red text-white hover:bg-red-600 transition-colors">
                        ${_t('webui.overwrite_btn')}
                    </button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        modal.querySelector('.cancel-btn').addEventListener('click', () => {
            modal.remove();
        });

        modal.querySelector('.overwrite-btn').addEventListener('click', async () => {
            modal.remove();
            await this.confirmCreate(type, name, true);
        });

        modal.addEventListener('click', (e) => {
            if (e.target === modal) modal.remove();
        });
    }

    async saveAllFiles() {
        if (this.modifiedFiles.size === 0) {
            this.showToast(_t('webui.no_files_to_save'), 'default');
            return;
        }

        let savedCount = 0;
        let errorCount = 0;

        for (const path of this.modifiedFiles) {
            const fileData = this.openFiles.get(path);
            if (!fileData) continue;

            const content = fileData.model.getValue();

            try {
                const response = await fetch(`/api/sessions/${this.sessionId}/files/content`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path, content })
                });

                if (response.ok) {
                    fileData.originalContent = content;
                    savedCount++;
                } else {
                    errorCount++;
                }
            } catch (error) {
                errorCount++;
            }
        }

        this.modifiedFiles.clear();
        this.updateTabs();

        if (errorCount > 0) {
            this.showToast(_t('webui.save_result', {saved: savedCount, errors: errorCount}), 'error');
        } else {
            this.showToast(_t('webui.files_saved', {count: savedCount}), 'success');
        }
    }

    toggleSessionsList() {
        const container = document.getElementById('sessionListContainer');
        const chevron = document.getElementById('sessionChevron');
        if (container) {
            const isHidden = container.classList.contains('hidden');
            container.classList.toggle('hidden');
            if (chevron) {
                chevron.style.transform = isHidden ? 'rotate(180deg)' : '';
            }
        }
    }

    // Resizers
    initResizers() {
        // Chat panel is on the left, resizer on its right - no reverse
        this.initResizer('chatResizer', this.chatPanel, 'width', 280, 500);
        // Explorer panel resizer
        this.initResizer('explorerResizer', this.explorerPanel, 'width', 150, 400);
    }

    initResizer(resizerId, panel, prop, min, max, reverse = false) {
        const resizer = document.getElementById(resizerId);
        if (!resizer) return;

        let startX, startSize;

        const onMouseMove = (e) => {
            const diff = reverse ? startX - e.clientX : e.clientX - startX;
            const newSize = Math.min(max, Math.max(min, startSize + diff));
            panel.style[prop] = newSize + 'px';
        };

        const onMouseUp = () => {
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            document.body.style.cursor = '';
            document.body.style.userSelect = '';
        };

        resizer.addEventListener('mousedown', (e) => {
            startX = e.clientX;
            startSize = parseInt(getComputedStyle(panel)[prop], 10);
            document.body.style.cursor = 'col-resize';
            document.body.style.userSelect = 'none';
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    }

    // Session Management
    async loadSessions() {
        try {
            const response = await fetch('/api/sessions');
            const data = await response.json();
            const sessions = data.sessions || [];
            this.renderSessions(sessions);

            if (sessions.length === 0) {
                await this.createSession();
            } else {
                this.selectSession(sessions[0].id);
            }
        } catch (error) {
            console.error('Failed to load sessions:', error);
        }
    }

    renderSessions(sessions) {
        // Store sessions for reference
        this.sessions = sessions;

        // Update session count
        const sessionCountEl = document.getElementById('sessionCount');
        if (sessionCountEl) {
            sessionCountEl.textContent = sessions.length;
        }

        // Update current session name
        this.updateCurrentSessionName();

        this.sessionList.innerHTML = sessions.map((session, index) => {
            const title = session.title || session.first_message || _t('webui.new_conversation', {n: sessions.length - index});
            const displayTitle = title.length > 28 ? title.substring(0, 28) + '...' : title;
            const isActive = session.id === this.sessionId;
            const date = new Date(session.created_at || Date.now());
            const timeStr = date.toLocaleDateString(_getLocaleTag(), { month: 'short', day: 'numeric' });

            return `
                <div class="session-item flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-vscode-active ${isActive ? 'bg-vscode-active' : ''}" data-id="${session.id}" data-title="${this.escapeHtml(title)}">
                    <div class="flex-1 min-w-0">
                        <div class="flex items-center gap-2">
                            <span class="truncate text-[13px] ${isActive ? 'text-white' : 'text-dark-300'}">${this.escapeHtml(displayTitle)}</span>
                        </div>
                        <div class="text-[10px] text-dark-500 mt-0.5">${timeStr}</div>
                    </div>
                    <button class="session-delete p-1 rounded hover:bg-red-500/30 text-dark-500 hover:text-red-400 opacity-0 group-hover:opacity-100" title="${_t('webui.delete_tooltip')}">
                        <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                        </svg>
                    </button>
                </div>
            `;
        }).join('');

        this.sessionList.querySelectorAll('.session-item').forEach(el => {
            el.addEventListener('click', (e) => {
                if (!e.target.closest('.session-delete')) {
                    this.selectSession(el.dataset.id);
                    this.toggleSessionsList(); // Close dropdown after selection
                }
            });
            el.querySelector('.session-delete')?.addEventListener('click', (e) => {
                e.stopPropagation();
                this.deleteSession(el.dataset.id);
            });
            // Show delete on hover
            el.addEventListener('mouseenter', () => {
                el.querySelector('.session-delete').style.opacity = '1';
            });
            el.addEventListener('mouseleave', () => {
                el.querySelector('.session-delete').style.opacity = '0';
            });
        });
    }

    updateCurrentSessionName() {
        // Session name is shown in the dropdown list, not in header anymore
    }

    async deleteSession(sessionId) {
        if (!confirm(_t('webui.confirm_delete_session'))) return;

        try {
            const response = await fetch(`/api/sessions/${sessionId}`, {
                method: 'DELETE'
            });

            if (!response.ok) {
                throw new Error('Failed to delete session');
            }

            // If deleting current session, switch to another or create new
            if (sessionId === this.sessionId) {
                this.sessionId = null;
                if (this.ws) {
                    this.ws.close();
                    this.ws = null;
                }
            }

            await this.loadSessions();
            this.showToast(_t('webui.session_deleted'), 'success');
        } catch (error) {
            console.error('Failed to delete session:', error);
            this.showToast(_t('webui.delete_failed'), 'error');
        }
    }

    async createSession() {
        try {
            const response = await fetch('/api/sessions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    working_directory: '.',
                    agent_id: 'agent',
                    model_name: null,
                    auto_approve: false,
                    approval_policy: this.approvalPolicy,
                })
            });
            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.detail || 'Failed to create session');
            }
            const session = await response.json();
            await this.loadSessions();
            this.selectSession(session.id);
            this.showToast(_t('webui.new_conversation_started'), 'success');
        } catch (error) {
            console.error('Failed to create session:', error);
            this.showToast(error.message || _t('webui.session_create_failed'), 'error');
        }
    }

    selectSession(sessionId) {
        this.sessionId = sessionId;
        this.lastSeq = this.loadLastSeqForSession(sessionId);

        // Update active state
        this.sessionList.querySelectorAll('.session-item').forEach(el => {
            const isActive = el.dataset.id === sessionId;
            el.classList.toggle('bg-vscode-active', isActive);
            el.querySelector('.truncate')?.classList.toggle('text-white', isActive);
            el.querySelector('.truncate')?.classList.toggle('text-dark-300', !isActive);
        });

        // Update current session name display
        this.updateCurrentSessionName();

        this.clearMessages();
        this.connectWebSocket();
        this.loadFiles();
    }

    // WebSocket Connection (W02: exponential backoff reconnection)
    connectWebSocket() {
        // Clear any pending reconnect timer
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }

        // Close existing connection
        if (this.ws) {
            this.ws.onclose = null; // Prevent reconnect loop
            this.ws.close();
            this.ws = null;
        }

        if (!this.sessionId) {
            console.warn('No session ID for WebSocket connection');
            return;
        }

        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const params = new URLSearchParams();
        if (this.lastSeq > 0) {
            params.set('last_seq', String(this.lastSeq));
        }
        const query = params.toString();
        const wsUrl = `${protocol}//${window.location.host}/ws/sessions/${this.sessionId}${query ? `?${query}` : ''}`;

        try {
            this.ws = new WebSocket(wsUrl);
        } catch (error) {
            console.error('WebSocket creation failed:', error);
            return;
        }

        const currentSessionId = this.sessionId;

        this.ws.onopen = () => {
            // Verify we're still on the same session
            if (this.sessionId !== currentSessionId) {
                this.ws?.close();
                return;
            }
            this.isConnected = true;
            // Reset backoff on successful connection
            this.reconnectAttempts = 0;
            this.connectionStatus.innerHTML = `<span class="w-2 h-2 rounded-full bg-green-400"></span> ${_t('webui.connected')}`;
            this.hideConnectionBanner();
            this.setInteractiveElementsEnabled(true);
            if (this.approvalPolicy) {
                this.ws.send(JSON.stringify({
                    type: 'session.set_approval_policy',
                    data: { approval_policy: this.approvalPolicy }
                }));
            }
        };

        this.ws.onclose = (event) => {
            this.isConnected = false;
            this.setInteractiveElementsEnabled(false);

            // Only reconnect if still on the same session and not a normal close
            if (this.sessionId === currentSessionId && event.code !== 1000 && event.code !== 4004) {
                if (this.reconnectAttempts < this.maxReconnectAttempts) {
                    // Exponential backoff: 2s, 4s, 8s, 16s, max 60s
                    const delay = Math.min(
                        this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts),
                        this.reconnectMaxDelay
                    );
                    this.reconnectAttempts++;
                    const delaySec = Math.round(delay / 1000);
                    this.connectionStatus.innerHTML = `<span class="w-2 h-2 rounded-full bg-yellow-400 animate-pulse"></span> ${_t('webui.reconnecting', {attempt: this.reconnectAttempts, max: this.maxReconnectAttempts})}`;
                    this.showConnectionBanner(_t('webui.connection_lost_delay', {delay: delaySec, attempt: this.reconnectAttempts, max: this.maxReconnectAttempts}), true);

                    this.reconnectTimer = setTimeout(() => {
                        if (this.sessionId === currentSessionId) {
                            console.log(`Reconnecting WebSocket (attempt ${this.reconnectAttempts})...`);
                            this.connectWebSocket();
                        }
                    }, delay);
                } else {
                    // Max retries exhausted
                    this.connectionStatus.innerHTML = `<span class="w-2 h-2 rounded-full bg-red-400"></span> ${_t('webui.disconnected')}`;
                    this.showConnectionBanner(_t('webui.connection_lost_retry'), true);
                }
            } else {
                this.connectionStatus.innerHTML = `<span class="w-2 h-2 rounded-full bg-red-400"></span> ${_t('webui.disconnected')}`;
            }
        };

        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
        };

        this.ws.onmessage = (event) => {
            let message;
            try {
                message = JSON.parse(event.data);
            } catch (e) {
                console.error('Failed to parse WebSocket message:', e);
                return;
            }
            this.handleWSMessage(message);
        };
    }

    handleWSMessage(message) {
        const { type } = message;
        const data = message.data || {};
        if (Number.isInteger(data._seq)) {
            this.lastSeq = Math.max(this.lastSeq, data._seq);
            this.persistLastSeqForSession();
        }

        switch (type) {
            case 'chat.message_start':
                this.startAssistantMessage();
                break;
            case 'chat.token':
                this.appendToken(data.content);
                break;
            case 'chat.message_end':
                this.endAssistantMessage();
                if (data.session_cost > 0) {
                    this.updateCostDisplay(data.session_cost);
                }
                break;
            case 'tool.start':
                this.showToolStart(data);
                break;
            case 'tool.success':
                this.showToolSuccess(data);
                break;
            case 'tool.error':
                this.showToolError(data);
                break;
            case 'approval.request':
                this.showApprovalRequest(data);
                break;
            case 'interview.questions':
                this.showInterviewRequest(data);
                break;
            case 'interview.complete':
                // Interview completed, resume streaming
                break;
            case 'file.changed':
                this.handleFileChange(data);
                break;
            case 'thinking.start':
                this.startThinking();
                break;
            case 'thinking.token':
                this.appendThinkingToken(data.content);
                break;
            case 'thinking.end':
                this.endThinking();
                break;
            case 'error':
                this.hideTypingIndicator();
                this.isStreaming = false;
                this.showToast(data.error || data.message || _t('webui.error_occurred'), 'error');
                break;
            case 'session.policy_updated':
                this.setApprovalPolicy(data.approval_policy, {
                    sendToServer: false,
                    notify: false,
                });
                break;
            case 'plan.mode_changed':
                this.setApprovalPolicy(data.approval_policy, {
                    sendToServer: false,
                    notify: false,
                });
                if (data.current_plan) {
                    this._currentPlan = data.current_plan;
                }
                break;

            case 'audit.replay_result':
                this.addSystemMessage(`Audit replay completed: ${(data.events || []).length} event(s)`);
                break;
        }
    }

    // Messaging (W03/W04: safeSend, E02: streaming message drop guard)
    sendMessage() {
        const content = this.messageInput.value.trim();
        if (!content || !this.isConnected) return;

        // Handle slash commands
        if (content.startsWith('/')) {
            this.handleSlashCommand(content);
            this.messageInput.value = '';
            this.messageInput.style.height = 'auto';
            return;
        }

        // E02: Prevent silent message drop during streaming
        if (this.isStreaming) {
            this.showToast(_t('webui.streaming_blocked'), 'error');
            // Do NOT clear the input - keep the user's message
            return;
        }

        // Build message with context
        let fullContent = content;
        const contextsCopy = [...this.contexts];

        if (this.contexts.length > 0) {
            // Prepend context to message
            const contextStr = this.contexts.map(ctx => {
                const filename = ctx.file.split('/').pop() || ctx.file.split('\\').pop();
                return `<context file="${ctx.file}" lines="${ctx.startLine}-${ctx.endLine}">\n${ctx.content}\n</context>`;
            }).join('\n\n');
            fullContent = contextStr + '\n\n' + content;
        }

        // Display message with context indicators
        this.removeWelcomeMessage(); // W07
        this.addMessageWithContext('user', content, contextsCopy);
        this.showTypingIndicator();

        // W03/W04: use safeSend
        if (!this.safeSend({ type: 'chat.send', data: { content: fullContent } })) {
            return;
        }

        // Clear contexts after sending
        this.contexts = [];
        this.renderContexts();

        this.messageInput.value = '';
        this.messageInput.style.height = 'auto';
        this.isStreaming = true;
    }

    addMessageWithContext(role, content, contexts = []) {
        this.removeWelcomeMessage(); // W07
        const welcomeMsg = this.messagesEl.querySelector('.flex.justify-center');
        if (welcomeMsg) welcomeMsg.remove();

        const messageEl = document.createElement('div');
        messageEl.className = `message message-${role} mb-3`;

        const now = new Date();
        const timeStr = now.toLocaleTimeString(_getLocaleTag(), { hour: '2-digit', minute: '2-digit', hour12: true });

        // Build context badges HTML
        let contextBadges = '';
        if (contexts.length > 0) {
            contextBadges = `
                <div class="flex flex-wrap gap-1 mb-2">
                    ${contexts.map(ctx => {
                        const filename = ctx.file.split('/').pop() || ctx.file.split('\\').pop();
                        return `
                            <span class="inline-flex items-center gap-1 bg-vscode-active/80 border border-vscode-border rounded px-2 py-0.5 text-xs">
                                <svg class="w-3 h-3 text-mx-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                                </svg>
                                <span class="text-mx-blue">${this.escapeHtml(filename)}</span>
                                <span class="text-dark-400">:${ctx.startLine}-${ctx.endLine}</span>
                            </span>
                        `;
                    }).join('')}
                </div>
            `;
        }

        messageEl.innerHTML = `
            <div class="flex justify-end">
                <div class="max-w-[85%]">
                    ${contextBadges}
                    <div class="bg-vscode-accent/20 rounded-lg px-4 py-3 border border-vscode-accent/30">
                        <div class="text-sm text-vscode-text whitespace-pre-wrap">${this.escapeHtml(content)}</div>
                    </div>
                    <div class="text-[10px] text-dark-500 mt-1 text-right">${timeStr}</div>
                </div>
            </div>
        `;

        this.messagesEl.appendChild(messageEl);
        this.scrollToBottom();
    }

    // Command Suggestions
    showCommandSuggestions(input) {
        const query = input.slice(1).toLowerCase(); // Remove leading /
        this.filteredCommands = this.commands.filter(c =>
            c.cmd.slice(1).toLowerCase().startsWith(query) ||
            c.desc.toLowerCase().includes(query)
        );

        if (this.filteredCommands.length === 0) {
            this.hideCommandSuggestions();
            return;
        }

        this.commandSuggestionIndex = 0;
        this.commandSuggestionVisible = true;
        this.renderCommandSuggestions();
    }

    hideCommandSuggestions() {
        this.commandSuggestionVisible = false;
        const container = document.getElementById('commandSuggestions');
        if (container) container.remove();
    }

    renderCommandSuggestions() {
        let container = document.getElementById('commandSuggestions');
        if (!container) {
            container = document.createElement('div');
            container.id = 'commandSuggestions';
            container.className = 'absolute bottom-full left-0 right-0 mb-1 bg-vscode-sidebar border border-vscode-border rounded-lg shadow-lg overflow-hidden z-50';
            this.messageInput.parentElement.style.position = 'relative';
            this.messageInput.parentElement.appendChild(container);
        }

        container.innerHTML = this.filteredCommands.map((cmd, i) => `
            <div class="command-suggestion flex items-center gap-3 px-3 py-2 cursor-pointer ${i === this.commandSuggestionIndex ? 'bg-vscode-active' : 'hover:bg-vscode-active/50'}"
                 data-index="${i}">
                <span class="text-lg">${cmd.icon}</span>
                <div class="flex-1">
                    <div class="text-sm text-vscode-text font-medium">${cmd.cmd}</div>
                    <div class="text-xs text-dark-400">${cmd.desc}</div>
                </div>
            </div>
        `).join('');

        // Bind click events
        container.querySelectorAll('.command-suggestion').forEach(el => {
            el.addEventListener('click', () => {
                this.commandSuggestionIndex = parseInt(el.dataset.index);
                this.selectCommandSuggestion();
            });
        });
    }

    navigateCommandSuggestion(direction) {
        this.commandSuggestionIndex += direction;
        if (this.commandSuggestionIndex < 0) {
            this.commandSuggestionIndex = this.filteredCommands.length - 1;
        } else if (this.commandSuggestionIndex >= this.filteredCommands.length) {
            this.commandSuggestionIndex = 0;
        }
        this.renderCommandSuggestions();
    }

    selectCommandSuggestion() {
        if (this.filteredCommands.length === 0) return;
        const selected = this.filteredCommands[this.commandSuggestionIndex];
        this.messageInput.value = selected.cmd + ' ';
        this.hideCommandSuggestions();
        this.messageInput.focus();
    }

    // File Suggestions (@)
    showFileSuggestions(input) {
        // Extract query after @
        const atIndex = input.lastIndexOf('@');
        if (atIndex === -1) return;
        const query = input.slice(atIndex + 1).toLowerCase();

        // Get all files from file tree
        const allFiles = this.getAllFiles();
        this.filteredFiles = allFiles.filter(f =>
            f.name.toLowerCase().includes(query) ||
            f.path.toLowerCase().includes(query)
        ).slice(0, 10); // Limit to 10 results

        if (this.filteredFiles.length === 0) {
            this.hideFileSuggestions();
            return;
        }

        this.fileSuggestionIndex = 0;
        this.fileSuggestionVisible = true;
        this.renderFileSuggestions();
    }

    hideFileSuggestions() {
        this.fileSuggestionVisible = false;
        const container = document.getElementById('fileSuggestions');
        if (container) container.remove();
    }

    getAllFiles() {
        // Recursively get all files from file tree
        const files = [];
        const collectFiles = (items, basePath = '') => {
            if (!items) return;
            items.forEach(item => {
                if (item.type === 'file') {
                    files.push({
                        name: item.name,
                        path: basePath + item.name
                    });
                } else if (item.type === 'directory' && item.children) {
                    collectFiles(item.children, basePath + item.name + '/');
                }
            });
        };
        collectFiles(this.fileTreeData);
        return files;
    }

    renderFileSuggestions() {
        let container = document.getElementById('fileSuggestions');
        if (!container) {
            container = document.createElement('div');
            container.id = 'fileSuggestions';
            container.className = 'absolute bottom-full left-0 right-0 mb-1 bg-vscode-sidebar border border-vscode-border rounded-lg shadow-lg overflow-hidden z-50 max-h-64 overflow-y-auto';
            this.messageInput.parentElement.style.position = 'relative';
            this.messageInput.parentElement.appendChild(container);
        }

        const iconColors = {
            'js': 'text-mx-yellow', 'ts': 'text-mx-blue', 'tsx': 'text-mx-blue',
            'py': 'text-mx-green', 'html': 'text-mx-orange', 'css': 'text-mx-blue',
            'json': 'text-mx-yellow', 'md': 'text-vscode-muted'
        };

        container.innerHTML = this.filteredFiles.map((file, i) => {
            const ext = file.name.split('.').pop()?.toLowerCase() || '';
            const iconColor = iconColors[ext] || 'text-vscode-muted';
            return `
                <div class="file-suggestion flex items-center gap-2 px-3 py-2 cursor-pointer ${i === this.fileSuggestionIndex ? 'bg-vscode-active' : 'hover:bg-vscode-active/50'}"
                     data-index="${i}" data-path="${this.escapeHtml(file.path)}">
                    <svg class="w-4 h-4 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                    </svg>
                    <div class="flex-1 min-w-0">
                        <div class="text-sm text-vscode-text truncate">${this.escapeHtml(file.name)}</div>
                        <div class="text-xs text-dark-400 truncate">${this.escapeHtml(file.path)}</div>
                    </div>
                </div>
            `;
        }).join('');

        // Bind click events
        container.querySelectorAll('.file-suggestion').forEach(el => {
            el.addEventListener('click', () => {
                this.fileSuggestionIndex = parseInt(el.dataset.index);
                this.selectFileSuggestion();
            });
        });
    }

    navigateFileSuggestion(direction) {
        this.fileSuggestionIndex += direction;
        if (this.fileSuggestionIndex < 0) {
            this.fileSuggestionIndex = this.filteredFiles.length - 1;
        } else if (this.fileSuggestionIndex >= this.filteredFiles.length) {
            this.fileSuggestionIndex = 0;
        }
        this.renderFileSuggestions();
    }

    async selectFileSuggestion() {
        if (this.filteredFiles.length === 0) return;
        const selected = this.filteredFiles[this.fileSuggestionIndex];

        // Remove @query from input
        const atIndex = this.messageInput.value.lastIndexOf('@');
        this.messageInput.value = this.messageInput.value.slice(0, atIndex);

        // Load file content and add to context
        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/content?path=${encodeURIComponent('/' + selected.path)}`);
            if (response.ok) {
                const data = await response.json();
                const content = data.content;
                const lines = content.split('\n');

                this.contexts.push({
                    file: selected.path,
                    startLine: 1,
                    endLine: lines.length,
                    content: content
                });

                this.renderContexts();
                this.showToast(_t('webui.file_added', {name: selected.name}), 'success');
            }
        } catch (error) {
            this.showToast(_t('webui.file_load_tree_failed'), 'error');
        }

        this.hideFileSuggestions();
        this.messageInput.focus();
    }

    handleSlashCommand(command) {
        const cmd = command.toLowerCase().split(' ')[0];
        const args = command.slice(cmd.length).trim();

        switch (cmd) {
            case '/help':
            case '/h':
                this.showCommandHelp();
                break;
            case '/new':
            case '/clear':
                this.createSession();
                this.addSystemMessage(_t('webui.new_conversation_started'));
                break;
            case '/model':
                if (args) {
                    this.changeModel(args);
                } else {
                    this.showSettingsModal();
                }
                break;
            case '/ralph':
            case '/ralph-loop':
                this.startRalphLoop(args);
                break;
            case '/stop':
                this.stopRalphLoop();
                break;
            case '/tokens':
                this.showTokenUsage();
                break;
            case '/cost':
                this.handleCostCommand();
                break;
            case '/session':
            case '/sessions':
                this.toggleSessionsList();
                break;
            case '/skills':
                this.showSkillsList();
                break;
            case '/skill':
                this.invokeSkill(args);
                break;
            case '/plan':
                this.handlePlanCommand(args);
                break;
            case '/approval':
                this.handleApprovalPolicyCommand(args);
                break;
            case '/audit':
                this.handleAuditCommand(args);
                break;
            case '/autocommit':
                this.handleAutocommitCommand();
                break;
            case '/undo':
                this.handleUndoCommand();
                break;
            case '/verify':
                this.handleVerifyCommand(args);
                break;
            case '/checkpoint':
                this.handleCheckpointCommand(args);
                break;
            case '/memory':
                this.handleMemoryCommand(args);
                break;
            case '/task':
                this.handleTaskCommand(args);
                break;
            default:
                this.addSystemMessage(_t('webui.unknown_command', {cmd: cmd}));
        }
    }

    handleApprovalPolicyCommand(args) {
        const raw = (args || '').trim();
        if (!raw) {
            this.addSystemMessage(
                `Current approval policy: ${this.approvalPolicy}\n` +
                'Usage: /approval <manual|read_only|safe_write|full_trusted|plan_only>'
            );
            return;
        }

        const normalized = this.normalizeApprovalPolicy(raw);
        if (!normalized) {
            this.addSystemMessage('Invalid policy. Use: /approval <manual|read_only|safe_write|full_trusted|plan_only>');
            return;
        }
        this.setApprovalPolicy(normalized, { notify: false });
        this.addSystemMessage(`Approval policy updated: ${normalized}`);
    }

    handlePlanCommand(args) {
        const raw = (args || '').trim();
        const sub = raw.split(/\s+/)[0]?.toLowerCase() || '';

        if (sub === 'show') {
            // Show current plan
            if (this._currentPlan) {
                this.addSystemMessage(`**Current Plan:**\n${this._currentPlan}`);
            } else {
                this.addSystemMessage('No plan captured yet.');
            }
            return;
        }

        if (sub === 'approve') {
            if (!this._currentPlan) {
                this.addSystemMessage('No plan to approve. Use /plan <task> first.');
                return;
            }
            this.safeSend({
                type: 'plan.approve',
                data: { task: this._planOriginalTask || '' }
            });
            this._currentPlan = null;
            this._planOriginalTask = null;
            this.addSystemMessage('Plan approved. Executing...');
            return;
        }

        if (sub === 'cancel') {
            this.safeSend({ type: 'plan.set_mode', data: { enable: false } });
            this._currentPlan = null;
            this._planOriginalTask = null;
            this.addSystemMessage('Plan mode cancelled.');
            return;
        }

        if (!raw) {
            // Toggle plan mode
            this.safeSend({ type: 'plan.set_mode', data: {} });
            return;
        }

        // /plan <task> — enter plan mode and send task
        this._planOriginalTask = raw;
        this.safeSend({ type: 'plan.set_mode', data: { enable: true } });
        // Send the task as a chat message (preamble injected server-side)
        this.safeSend({ type: 'chat.send', data: { content: raw } });
        this.addSystemMessage(`Plan mode ON. Planning: ${raw}`);
    }

    handleAuditCommand(args) {
        const text = (args || '').trim();
        const parts = text ? text.split(/\s+/) : [];
        const action = parts[0] || '';
        if (action.toLowerCase() !== 'replay') {
            this.addSystemMessage('Usage: /audit replay [limit]');
            return;
        }
        let limit = 200;
        if (parts.length >= 2) {
            const parsed = Number.parseInt(parts[1], 10);
            if (Number.isFinite(parsed) && parsed > 0) {
                limit = Math.min(parsed, 1000);
            }
        }
        if (!this.safeSend({ type: 'audit.replay', data: { limit } })) {
            return;
        }
        this.addSystemMessage(`Requested audit replay (limit=${limit})`);
    }

    // ── Git Auto-commit ──────────────────────────────────────────────────
    async handleCostCommand() {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        try {
            const resp = await fetch(`/api/sessions/${this.sessionId}/cost`);
            if (resp.ok) {
                const data = await resp.json();
                const model = data.model_name || 'unknown';
                const cost = data.session_cost > 0 ? `$${data.session_cost.toFixed(4)}` : '$0.00';
                const pricing = data.pricing_available
                    ? `(input: $${data.input_price_per_1m}/1M, output: $${data.output_price_per_1m}/1M)`
                    : '(no pricing data)';
                this.addSystemMessage(`Model: ${model}\nSession cost: ${cost}\nPricing: ${pricing}`);
            } else {
                this.addSystemMessage('Failed to get cost info.');
            }
        } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
    }

    async handleAutocommitCommand() {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        try {
            const resp = await fetch(`/api/sessions/${this.sessionId}/git/autocommit`, { method: 'POST' });
            if (resp.ok) {
                const data = await resp.json();
                this.addSystemMessage(`Auto-commit: ${data.enabled ? 'ON' : 'OFF'}`);
            } else {
                this.addSystemMessage('Failed to toggle auto-commit.');
            }
        } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
    }

    async handleUndoCommand() {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        try {
            const resp = await fetch(`/api/sessions/${this.sessionId}/git/undo`, { method: 'POST' });
            if (resp.ok) {
                const data = await resp.json();
                this.addSystemMessage(data.success ? `Undo: ${data.message}` : `Undo failed: ${data.message}`);
            } else {
                this.addSystemMessage('Failed to undo.');
            }
        } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
    }

    // ── Verify Loop ──────────────────────────────────────────────────────
    async handleVerifyCommand(args) {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        const sub = (args || '').trim().toLowerCase();
        const base = `/api/sessions/${this.sessionId}/verify`;

        if (sub === 'run') {
            this.addSystemMessage('Running verification...');
            try {
                const resp = await fetch(`${base}/run`, { method: 'POST' });
                if (resp.ok) {
                    const data = await resp.json();
                    this.addSystemMessage(data.summary);
                    if (!data.all_passed && data.errors.length > 0) {
                        this.addSystemMessage('```\n' + data.errors.join('\n') + '\n```');
                    }
                } else { this.addSystemMessage('Verify run failed.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'status') {
            try {
                const resp = await fetch(base);
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(
                        `Auto-verify: ${d.enabled ? 'ON' : 'OFF'}\n` +
                        `Test: ${d.test_command || '(none)'}\nLint: ${d.lint_command || '(none)'}`
                    );
                } else { this.addSystemMessage('Failed to get verify status.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub.startsWith('set-test ') || sub.startsWith('set-lint ')) {
            const isTest = sub.startsWith('set-test ');
            const val = args.trim().slice(isTest ? 9 : 9).trim();
            if (!val) { this.addSystemMessage(`Usage: /verify ${isTest ? 'set-test' : 'set-lint'} <cmd>`); return; }
            const body = isTest ? { test_command: val } : { lint_command: val };
            try {
                const resp = await fetch(base, {
                    method: 'PATCH',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(body),
                });
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(`${isTest ? 'Test' : 'Lint'} command set: ${val}`);
                } else { this.addSystemMessage('Failed to update config.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        // Toggle (no args or unknown)
        if (!sub) {
            try {
                const resp = await fetch(`${base}/toggle`, { method: 'POST' });
                if (resp.ok) {
                    const d = await resp.json();
                    let detail = '';
                    if (d.enabled) {
                        const parts = [];
                        if (d.test_command) parts.push(`test: ${d.test_command}`);
                        if (d.lint_command) parts.push(`lint: ${d.lint_command}`);
                        detail = parts.length ? ` (${parts.join(', ')})` : ' (no commands detected)';
                    }
                    this.addSystemMessage(`Auto-verify: ${d.enabled ? 'ON' : 'OFF'}${detail}`);
                } else { this.addSystemMessage('Failed to toggle verify.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        this.addSystemMessage('Usage: /verify [run|status|set-test <cmd>|set-lint <cmd>]');
    }

    // ── Checkpoints ──────────────────────────────────────────────────────
    async handleCheckpointCommand(args) {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        const parts = (args || '').trim().split(/\s+/);
        const sub = parts[0]?.toLowerCase() || '';
        const base = `/api/sessions/${this.sessionId}/checkpoints`;

        if (sub === 'list' || sub === 'ls') {
            try {
                const resp = await fetch(base);
                if (resp.ok) {
                    const list = await resp.json();
                    if (list.length === 0) { this.addSystemMessage('No saved checkpoints.'); return; }
                    const lines = ['Saved checkpoints:\n'];
                    list.forEach(cp => {
                        const desc = cp.description ? ` - ${cp.description}` : '';
                        const ts = (cp.created_at || '').slice(0, 19).replace('T', ' ');
                        lines.push(`  ${cp.label}  ${ts}${desc}`);
                    });
                    this.addSystemMessage(lines.join('\n'));
                } else { this.addSystemMessage('Failed to list checkpoints.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'save') {
            const label = parts[1];
            if (!label) { this.addSystemMessage('Usage: /checkpoint save <name> [desc]'); return; }
            const desc = parts.slice(2).join(' ');
            try {
                const resp = await fetch(base, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ label, description: desc }),
                });
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(`Checkpoint '${d.label}' saved (id: ${d.checkpoint_id.slice(0, 12)}...)`);
                } else {
                    const err = await resp.json().catch(() => ({}));
                    this.addSystemMessage(err.detail || 'Failed to save checkpoint.');
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'restore') {
            const label = parts[1];
            if (!label) { this.addSystemMessage('Usage: /checkpoint restore <name>'); return; }
            try {
                const resp = await fetch(`${base}/restore`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ label }),
                });
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(d.message);
                } else {
                    const err = await resp.json().catch(() => ({}));
                    this.addSystemMessage(err.detail || 'Restore failed.');
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'delete' || sub === 'rm') {
            const label = parts[1];
            if (!label) { this.addSystemMessage('Usage: /checkpoint delete <name>'); return; }
            try {
                const resp = await fetch(`${base}/${encodeURIComponent(label)}`, { method: 'DELETE' });
                if (resp.ok) {
                    this.addSystemMessage(`Checkpoint '${label}' deleted.`);
                } else {
                    const err = await resp.json().catch(() => ({}));
                    this.addSystemMessage(err.detail || 'Delete failed.');
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        this.addSystemMessage(
            'Usage:\n  /checkpoint save <name> [desc]\n  /checkpoint list\n' +
            '  /checkpoint restore <name>\n  /checkpoint delete <name>'
        );
    }

    // ── Memory ──────────────────────────────────────────────────────────
    async handleMemoryCommand(args) {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        const parts = (args || '').trim().split(/\s+/);
        const sub = parts[0]?.toLowerCase() || '';
        const base = `/api/sessions/${this.sessionId}/memory`;

        if (!sub || sub === 'list' || sub === 'ls') {
            try {
                const resp = await fetch(base);
                if (resp.ok) {
                    const list = await resp.json();
                    if (list.length === 0) {
                        this.addSystemMessage('No memories stored yet.\nUse /memory add <key> <content> to add.');
                        return;
                    }
                    const lines = ['Stored memories:\n'];
                    list.forEach(m => {
                        let preview = m.preview || '';
                        if (preview.length > 60) preview = preview.slice(0, 60) + '...';
                        lines.push(`  ${m.key}  ${preview}`);
                    });
                    lines.push(`\nTotal: ${list.length} entries`);
                    this.addSystemMessage(lines.join('\n'));
                } else { this.addSystemMessage('Failed to list memories.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'show') {
            const key = parts[1];
            if (!key) { this.addSystemMessage('Usage: /memory show <key>'); return; }
            try {
                const resp = await fetch(`${base}/${encodeURIComponent(key)}`);
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(`Memory: ${d.key}\n\n${d.content}`);
                } else {
                    this.addSystemMessage(`Memory '${key}' not found.`);
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'add') {
            const key = parts[1];
            const content = parts.slice(2).join(' ');
            if (!key || !content) { this.addSystemMessage('Usage: /memory add <key> <content>'); return; }
            try {
                const resp = await fetch(base, {
                    method: 'PUT',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ key, content }),
                });
                if (resp.ok) {
                    this.addSystemMessage(`Memory '${key}' saved.`);
                } else { this.addSystemMessage('Failed to save memory.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'delete' || sub === 'rm') {
            const key = parts[1];
            if (!key) { this.addSystemMessage('Usage: /memory delete <key>'); return; }
            try {
                const resp = await fetch(`${base}/${encodeURIComponent(key)}`, { method: 'DELETE' });
                if (resp.ok) {
                    this.addSystemMessage(`Memory '${key}' deleted.`);
                } else {
                    this.addSystemMessage(`Memory '${key}' not found.`);
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'clear') {
            try {
                const resp = await fetch(base, { method: 'DELETE' });
                if (resp.ok) {
                    const d = await resp.json();
                    this.addSystemMessage(`Cleared ${d.cleared} memory entries.`);
                } else { this.addSystemMessage('Failed to clear memories.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        this.addSystemMessage(
            'Usage:\n  /memory             - List memories\n  /memory show <key>  - Show entry\n' +
            '  /memory add <key> <val> - Add entry\n  /memory delete <key>  - Delete entry\n  /memory clear  - Clear all'
        );
    }

    // ── Background Tasks ────────────────────────────────────────────────
    async handleTaskCommand(args) {
        if (!this.sessionId) { this.addSystemMessage('No active session.'); return; }
        const parts = (args || '').trim().split(/\s+/);
        const sub = parts[0]?.toLowerCase() || '';
        const base = `/api/sessions/${this.sessionId}/tasks`;

        if (sub === 'list' || sub === 'ls') {
            try {
                const resp = await fetch(base);
                if (resp.ok) {
                    const list = await resp.json();
                    if (list.length === 0) { this.addSystemMessage('No background tasks.'); return; }
                    const icons = { queued: '⏳', running: '🔄', completed: '✅', failed: '❌', cancelled: '⛔' };
                    const lines = ['Background tasks:\n'];
                    list.forEach(t => {
                        const icon = icons[t.status] || '?';
                        let desc = t.description || '';
                        if (desc.length > 50) desc = desc.slice(0, 50) + '...';
                        lines.push(`  ${icon} ${t.id}  ${t.status}  ${desc}`);
                    });
                    this.addSystemMessage(lines.join('\n'));
                } else { this.addSystemMessage('Failed to list tasks.'); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'status') {
            const taskId = parts[1];
            if (!taskId) { this.addSystemMessage('Usage: /task status <id>'); return; }
            try {
                const resp = await fetch(`${base}/${taskId}`);
                if (resp.ok) {
                    const t = await resp.json();
                    const lines = [
                        `Task: ${t.id}`, `Status: ${t.status}`,
                        `Description: ${t.description}`, `Created: ${(t.created_at || '').slice(0, 19)}`,
                    ];
                    if (t.started_at) lines.push(`Started: ${t.started_at.slice(0, 19)}`);
                    if (t.completed_at) lines.push(`Completed: ${t.completed_at.slice(0, 19)}`);
                    if (t.result_summary) lines.push(`Result: ${t.result_summary}`);
                    if (t.error) lines.push(`Error: ${t.error}`);
                    this.addSystemMessage(lines.join('\n'));
                } else { this.addSystemMessage(`Task '${taskId}' not found.`); }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        if (sub === 'cancel') {
            const taskId = parts[1];
            if (!taskId) { this.addSystemMessage('Usage: /task cancel <id>'); return; }
            try {
                const resp = await fetch(`${base}/${taskId}`, { method: 'DELETE' });
                if (resp.ok) {
                    this.addSystemMessage(`Task '${taskId}' cancelled.`);
                } else {
                    const err = await resp.json().catch(() => ({}));
                    this.addSystemMessage(err.detail || `Cannot cancel task '${taskId}'.`);
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        // /task <description> — submit via WS or REST
        if (sub && sub !== 'help') {
            const description = (args || '').trim();
            try {
                const resp = await fetch(base, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ description }),
                });
                if (resp.ok) {
                    const t = await resp.json();
                    this.addSystemMessage(`Background task created: ${t.id}\nUse /task list to check progress.`);
                } else {
                    const err = await resp.json().catch(() => ({}));
                    this.addSystemMessage(err.detail || 'Failed to create task.');
                }
            } catch (e) { this.addSystemMessage(`Error: ${e.message}`); }
            return;
        }

        this.addSystemMessage(
            'Usage:\n  /task <description>  - Submit background task\n  /task list  - List tasks\n' +
            '  /task status <id>  - Show task status\n  /task cancel <id>  - Cancel task'
        );
    }

    async fetchSkills() {
        if (this.skillsCache) {
            return this.skillsCache;
        }
        try {
            const response = await fetch('/api/skills');
            if (response.ok) {
                this.skillsCache = await response.json();
                return this.skillsCache;
            }
        } catch (error) {
            console.error('Failed to fetch skills:', error);
        }
        return [];
    }

    async showSkillsList() {
        const skills = await this.fetchSkills();
        if (skills.length === 0) {
            this.addSystemMessage(_t('webui.no_skills_available'));
            return;
        }

        const lines = [_t('webui.available_skills')];

        // Group by source
        const builtin = skills.filter(s => s.source === 'builtin');
        const user = skills.filter(s => s.source === 'user');
        const project = skills.filter(s => s.source === 'project');

        if (builtin.length > 0) {
            lines.push(_t('webui.builtin_skills'));
            builtin.forEach(s => {
                lines.push(`  • \`${s.name}\` - ${s.description.slice(0, 50)}...`);
            });
            lines.push('');
        }

        if (user.length > 0) {
            lines.push(_t('webui.user_skills'));
            user.forEach(s => {
                lines.push(`  • \`${s.name}\` - ${s.description.slice(0, 50)}...`);
            });
            lines.push('');
        }

        if (project.length > 0) {
            lines.push(_t('webui.project_skills'));
            project.forEach(s => {
                lines.push(`  • \`${s.name}\` - ${s.description.slice(0, 50)}...`);
            });
            lines.push('');
        }

        lines.push(_t('webui.skill_invoke_hint'));
        this.addSystemMessage(lines.join('\n'));
    }

    async invokeSkill(args) {
        if (!args || args.trim() === '') {
            this.showSkillsList();
            return;
        }

        const parts = args.trim().split(/\s+/);
        const skillName = parts[0];
        const task = parts.slice(1).join(' ');

        try {
            const response = await fetch('/api/skills/invoke', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ name: skillName, task: task })
            });

            if (!response.ok) {
                const error = await response.json();
                this.addSystemMessage(_t('webui.skill_error', {detail: error.detail}));
                return;
            }

            const result = await response.json();

            // Display skill invocation info
            this.addSystemMessage(`🎯 ${_t('webui.skill_running', {name: skillName})}`);

            // Send the skill prompt to the agent (W03/W04: safeSend)
            this.showTypingIndicator();
            if (this.safeSend({ type: 'chat.send', data: { content: result.prompt } })) {
                this.isStreaming = true;
            }
        } catch (error) {
            console.error('Skill invocation error:', error);
            this.addSystemMessage(_t('webui.skill_exec_error', {error: error.message}));
        }
    }

    showCommandHelp() {
        const helpText = [
            _t('webui.help_title'),
            '',
            _t('webui.help_session_mgmt'),
            '  ' + _t('webui.help_new_clear'),
            '  ' + _t('webui.help_sessions'),
            '',
            _t('webui.help_settings'),
            '  ' + _t('webui.help_model'),
            '  ' + _t('webui.help_tokens'),
            '',
            _t('webui.help_skills_section'),
            '  ' + _t('webui.help_skills_list'),
            '  ' + _t('webui.help_skill_run'),
            '',
            _t('webui.help_ralph_section'),
            '  ' + _t('webui.help_ralph'),
            '  ' + _t('webui.help_ralph_iterations'),
            '  ' + _t('webui.help_stop'),
            '',
            _t('webui.help_misc'),
            '  ' + _t('webui.help_help'),
            '',
            _t('webui.help_shortcuts_section'),
            '  ' + _t('webui.help_ctrl_l'),
            '  ' + _t('webui.help_ctrl_s'),
        ].join('\n');
        this.addSystemMessage(helpText);
    }

    addSystemMessage(content) {
        const messageEl = document.createElement('div');
        messageEl.className = 'message message-system mb-3';
        messageEl.innerHTML = `
            <div class="bg-vscode-active/50 border border-vscode-border rounded-lg px-4 py-3">
                <div class="text-sm text-vscode-text whitespace-pre-wrap">${this.renderMarkdown(content)}</div>
            </div>
        `;
        this.messagesEl.appendChild(messageEl);
        this.scrollToBottom();
    }

    async changeModel(modelSpec) {
        // Parse model spec: provider/model or just model name
        let provider = 'ollama';
        let model = modelSpec;

        if (modelSpec.includes('/')) {
            [provider, model] = modelSpec.split('/', 2);
        }

        this.modelSettings.provider = provider;
        this.modelSettings.model = model;

        try {
            await fetch('/api/sessions/settings/global', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    provider: provider,
                    base_url: this.modelSettings.baseUrl || null,
                    api_key: this.modelSettings.apiKey || null,
                    model: model,
                    save_to: 'global'
                })
            });
            this.updateModelDisplay();
            this.addSystemMessage(_t('webui.model_changed', {spec: `${provider}/${model}`}));
        } catch (error) {
            this.addSystemMessage(_t('webui.model_change_failed', {error: error.message}));
        }
    }

    showTokenUsage() {
        // Token usage would need to be tracked from WebSocket responses
        this.addSystemMessage(_t('webui.token_usage_unsupported'));
    }

    // Ralph Loop
    startRalphLoop(args) {
        if (!args) {
            this.addSystemMessage(_t('webui.ralph_usage'));
            return;
        }

        // Parse iterations
        let task = args;
        let maxIterations = 0;

        if (args.includes('--iterations ')) {
            const parts = args.split('--iterations ');
            task = parts[0].trim();
            maxIterations = parseInt(parts[1].split(' ')[0]) || 0;
        } else if (args.includes('-i ')) {
            const parts = args.split('-i ');
            task = parts[0].trim();
            maxIterations = parseInt(parts[1].split(' ')[0]) || 0;
        }

        this.ralphLoop = {
            active: true,
            task: task,
            iteration: 0,
            maxIterations: maxIterations
        };

        const iterDisplay = maxIterations > 0 ? _t('webui.ralph_iterations_count', {n: maxIterations}) : _t('webui.ralph_iterations_unlimited');
        this.addSystemMessage(`🔄 ${_t('webui.ralph_mode_start', {task: task, iterations: iterDisplay})}`);

        // Send first message
        this.addMessage('user', task);
        this.showTypingIndicator();
        // W03/W04: use safeSend
        if (!this.safeSend({ type: 'chat.send', data: { content: task, ralph_mode: true } })) {
            return;
        }
        this.isStreaming = true;
    }

    stopRalphLoop() {
        if (this.ralphLoop?.active) {
            this.ralphLoop.active = false;
            this.addSystemMessage(`🛑 ${_t('webui.ralph_mode_stop', {count: this.ralphLoop.iteration})}`);
        } else {
            this.addSystemMessage(_t('webui.ralph_no_active'));
        }
    }

    continueRalphLoop() {
        if (!this.ralphLoop?.active) return;

        this.ralphLoop.iteration++;

        // Check max iterations
        if (this.ralphLoop.maxIterations > 0 && this.ralphLoop.iteration >= this.ralphLoop.maxIterations) {
            this.addSystemMessage(`✅ ${_t('webui.ralph_complete', {count: this.ralphLoop.iteration})}`);
            this.ralphLoop.active = false;
            return;
        }

        // Continue with next iteration
        const continuePrompt = `Continue with the task. Current iteration: ${this.ralphLoop.iteration + 1}`;
        setTimeout(() => {
            if (this.ralphLoop?.active) {
                this.addSystemMessage(`🔄 ${_t('webui.ralph_iteration', {n: this.ralphLoop.iteration + 1})}`);
                // W03/W04: use safeSend
                if (!this.safeSend({ type: 'chat.send', data: { content: continuePrompt, ralph_mode: true } })) {
                    return;
                }
                this.isStreaming = true;
            }
        }, 1000);
    }

    addMessage(role, content) {
        this.removeWelcomeMessage(); // W07
        const welcomeMsg = this.messagesEl.querySelector('.flex.justify-center');
        if (welcomeMsg) welcomeMsg.remove();

        const messageEl = document.createElement('div');
        messageEl.className = `message message-${role} mb-3`;

        const now = new Date();
        const timeStr = now.toLocaleTimeString(_getLocaleTag(), { hour: '2-digit', minute: '2-digit', hour12: true });

        if (role === 'user') {
            messageEl.innerHTML = `
                <div class="flex justify-end">
                    <div class="max-w-[85%]">
                        <div class="bg-vscode-accent text-white rounded-lg rounded-tr-sm px-3 py-2 text-sm">
                            <p class="whitespace-pre-wrap">${this.escapeHtml(content)}</p>
                        </div>
                        <div class="flex justify-end mt-1">
                            <span class="text-[10px] text-dark-500">${timeStr}</span>
                        </div>
                    </div>
                </div>
            `;
        } else {
            const modelName = this.modelSettings.model || 'AI';
            messageEl.innerHTML = `
                <div class="max-w-[85%]">
                    <div class="bg-vscode-active border border-vscode-border rounded-lg rounded-tl-sm px-3 py-2 text-sm">
                        <div class="message-content markdown-content"></div>
                    </div>
                    <div class="flex items-center gap-2 mt-1">
                        <span class="text-[10px] text-vscode-muted">${timeStr}</span>
                        <span class="text-[10px] text-vscode-muted">·</span>
                        <span class="text-[10px] text-mx-blue">${this.escapeHtml(modelName)}</span>
                    </div>
                </div>
            `;
        }

        this.messagesEl.appendChild(messageEl);
        this.scrollToBottom();
        return messageEl;
    }

    startAssistantMessage() {
        // Remove typing indicator if present
        const typingEl = this.messagesEl.querySelector('.typing-indicator');
        if (typingEl) typingEl.remove();

        this.currentMessageEl = this.addMessage('assistant', '');
        this.currentContent = '';
    }

    showTypingIndicator() {
        // Remove existing typing indicator
        const existing = this.messagesEl.querySelector('.typing-indicator');
        if (existing) return;

        const typingEl = document.createElement('div');
        typingEl.className = 'typing-indicator message mb-3';
        typingEl.innerHTML = `
            <div class="max-w-[85%]">
                <div class="bg-vscode-active border border-vscode-border rounded-lg rounded-tl-sm px-3 py-2 text-sm inline-block">
                    <div class="flex items-center gap-1">
                        <span class="w-2 h-2 bg-dark-400 rounded-full animate-bounce" style="animation-delay: 0ms"></span>
                        <span class="w-2 h-2 bg-dark-400 rounded-full animate-bounce" style="animation-delay: 150ms"></span>
                        <span class="w-2 h-2 bg-dark-400 rounded-full animate-bounce" style="animation-delay: 300ms"></span>
                    </div>
                </div>
            </div>
        `;
        this.messagesEl.appendChild(typingEl);
        this.scrollToBottom();
    }

    hideTypingIndicator() {
        const typingEl = this.messagesEl.querySelector('.typing-indicator');
        if (typingEl) typingEl.remove();
    }

    appendToken(token) {
        if (!this.currentMessageEl) return;
        this.currentContent += token;
        const contentEl = this.currentMessageEl.querySelector('.message-content');
        contentEl.innerHTML = this.renderMarkdown(this.currentContent);
        this.scrollToBottom();
    }

    endAssistantMessage() {
        this.hideTypingIndicator();
        this.currentMessageEl = null;
        this.currentContent = '';
        this.isStreaming = false;

        // Continue Ralph loop if active
        if (this.ralphLoop?.active) {
            this.continueRalphLoop();
        }
    }

    updateCostDisplay(sessionCost) {
        let costEl = document.getElementById('sessionCostDisplay');
        if (!costEl) {
            // Create the cost display element next to the approval toggle
            const toolbar = document.querySelector('.chat-toolbar') || document.querySelector('.toolbar');
            if (toolbar) {
                costEl = document.createElement('span');
                costEl.id = 'sessionCostDisplay';
                costEl.className = 'text-xs opacity-60 ml-2';
                costEl.title = 'Session cost';
                toolbar.appendChild(costEl);
            }
        }
        if (costEl) {
            if (sessionCost >= 1.0) {
                costEl.textContent = '$' + sessionCost.toFixed(2);
                costEl.className = 'text-xs text-yellow-400 font-medium ml-2';
            } else {
                costEl.textContent = '$' + sessionCost.toFixed(4);
                costEl.className = 'text-xs opacity-60 ml-2';
            }
        }
    }

    // Thinking
    startThinking() {
        this.isThinking = true;
        const welcomeMsg = this.messagesEl.querySelector('.flex.justify-center');
        if (welcomeMsg) welcomeMsg.remove();

        const now = new Date();
        const timeStr = now.toLocaleTimeString(_getLocaleTag(), { hour: '2-digit', minute: '2-digit', hour12: true });
        const modelName = this.modelSettings.model || 'AI';

        this.thinkingMessageEl = document.createElement('div');
        this.thinkingMessageEl.className = 'message message-assistant mb-3';
        this.thinkingMessageEl.innerHTML = `
            <div class="max-w-[85%]">
                <div class="bg-vscode-active border border-vscode-border rounded-lg rounded-tl-sm px-3 py-2 text-sm">
                    <div class="thinking-indicator flex items-center gap-2 text-mx-blue">
                        <svg class="w-4 h-4 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
                        </svg>
                        <span>${_t('webui.thinking')}</span>
                    </div>
                    <div class="thinking-content text-dark-500 text-xs mt-2 hidden"></div>
                </div>
                <div class="flex items-center gap-2 mt-1">
                    <span class="text-[10px] text-vscode-muted">${timeStr}</span>
                    <span class="text-[10px] text-vscode-muted">·</span>
                    <span class="text-[10px] text-mx-blue">${this.escapeHtml(modelName)}</span>
                </div>
            </div>
        `;

        this.messagesEl.appendChild(this.thinkingMessageEl);
        this.scrollToBottom();
    }

    appendThinkingToken(token) {
        if (!this.thinkingMessageEl) return;
        const contentEl = this.thinkingMessageEl.querySelector('.thinking-content');
        if (contentEl) {
            contentEl.classList.remove('hidden');
            contentEl.textContent += token;
            this.scrollToBottom();
        }
    }

    endThinking() {
        this.isThinking = false;
        if (this.thinkingMessageEl) {
            const content = this.thinkingMessageEl.querySelector('.thinking-content');
            if (!content?.textContent.trim()) {
                this.thinkingMessageEl.remove();
            } else {
                const header = this.thinkingMessageEl.querySelector('.thinking-indicator');
                if (header) {
                    header.innerHTML = `
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
                        </svg>
                        <span>${_t('webui.thinking_done')}</span>
                    `;
                    header.classList.remove('text-mx-blue');
                    header.classList.add('text-vscode-muted', 'cursor-pointer');
                    header.addEventListener('click', () => {
                        content.classList.toggle('hidden');
                    });
                }
                content.classList.add('hidden');
            }
            this.thinkingMessageEl = null;
        }
    }

    // Tool Execution
    showToolStart(data) {
        const toolEl = document.createElement('div');
        toolEl.className = 'tool-message mb-2';
        toolEl.id = `tool-${data.tool_id}`;
        toolEl.innerHTML = `
            <div class="inline-flex items-center gap-2 bg-vscode-active/50 border border-vscode-border rounded-md px-3 py-1.5 text-xs">
                <span class="text-mx-cyan">⚡</span>
                <span class="text-vscode-text font-medium">${this.escapeHtml(data.tool_name)}</span>
                <span class="tool-status text-vscode-muted animate-pulse">${_t('webui.tool_running')}</span>
            </div>
        `;
        this.messagesEl.appendChild(toolEl);
        this.scrollToBottom();
    }

    showToolSuccess(data) {
        const toolEl = document.getElementById(`tool-${data.tool_id}`);
        if (!toolEl) return;
        const statusEl = toolEl.querySelector('.tool-status');
        if (statusEl) {
            statusEl.classList.remove('animate-pulse', 'text-vscode-muted');
            statusEl.classList.add('text-mx-green');
            statusEl.textContent = _t('webui.tool_complete');
        }
        // E05: Show tool output with size limit
        if (data.output) {
            this._appendToolOutput(toolEl, data.output);
        }
    }

    showToolError(data) {
        const toolEl = document.getElementById(`tool-${data.tool_id}`);
        if (!toolEl) return;
        const statusEl = toolEl.querySelector('.tool-status');
        if (statusEl) {
            statusEl.classList.remove('animate-pulse', 'text-vscode-muted');
            statusEl.classList.add('text-mx-red');
            statusEl.textContent = _t('webui.tool_error');
        }
        // E05: Show tool error output with size limit
        if (data.output || data.error) {
            this._appendToolOutput(toolEl, data.output || data.error);
        }
    }

    // E05: Append tool output with 10,000 character truncation
    _appendToolOutput(toolEl, output) {
        const TOOL_OUTPUT_MAX_CHARS = 10000;
        const fullOutput = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
        const totalChars = fullOutput.length;
        const isTruncated = totalChars > TOOL_OUTPUT_MAX_CHARS;
        const displayOutput = isTruncated ? fullOutput.substring(0, TOOL_OUTPUT_MAX_CHARS) : fullOutput;

        const wrapper = document.createElement('div');
        wrapper.className = 'tool-output-wrapper';

        const contentEl = document.createElement('div');
        contentEl.className = 'tool-output-content';
        contentEl.textContent = displayOutput;
        wrapper.appendChild(contentEl);

        if (isTruncated) {
            const truncMsg = document.createElement('div');
            truncMsg.className = 'tool-output-truncated';
            truncMsg.textContent = `... (truncated, ${totalChars.toLocaleString()} chars total). Click below to copy full output.`;
            wrapper.appendChild(truncMsg);
        }

        const copyBtn = document.createElement('button');
        copyBtn.className = 'tool-output-copy-btn';
        copyBtn.innerHTML = `<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg> Copy full output`;
        copyBtn.addEventListener('click', () => {
            navigator.clipboard.writeText(fullOutput).then(() => {
                copyBtn.innerHTML = `<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Copied!`;
                setTimeout(() => {
                    copyBtn.innerHTML = `<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg> Copy full output`;
                }, 2000);
            });
        });
        wrapper.appendChild(copyBtn);

        toolEl.appendChild(wrapper);
    }

    // Approval (inline in chat) - W09: ARIA, E04: duplicate check, E09: .approval-buttons class
    showApprovalRequest(data) {
        this.hideTypingIndicator();
        this.pendingApproval = data;

        const approvalId = `approval-${data.interrupt_id || Date.now()}`;

        // E04: Prevent duplicate approval elements on reconnect
        const existing = document.getElementById(approvalId);
        if (existing) existing.remove();

        const approvalEl = document.createElement('div');
        approvalEl.className = 'approval-request message mb-3';
        approvalEl.id = approvalId;
        // W09: ARIA alertdialog role
        approvalEl.setAttribute('role', 'alertdialog');
        approvalEl.setAttribute('aria-label', _t('webui.approval_required'));

        const toolName = data.tool_name || 'Tool Call';
        const actionRequests = data.action_requests || [data];

        // Format the action details
        let detailsHtml = '';
        for (const action of actionRequests) {
            const name = action.tool_name || action.action?.tool || toolName;
            const args = action.tool_args || action.action?.tool_input || action.args || {};
            const preview = action.preview;

            detailsHtml += `<div class="mb-2">`;
            detailsHtml += `<div class="text-xs text-vscode-accent font-medium">${this.escapeHtml(name)}</div>`;

            // Show diff if available
            if (preview?.diff) {
                detailsHtml += `<div class="mt-2 text-[11px] font-mono bg-vscode-bg rounded border border-vscode-border overflow-hidden">`;
                detailsHtml += this.renderDiff(preview.diff);
                detailsHtml += `</div>`;
            } else if (preview?.error) {
                detailsHtml += `<div class="mt-1 text-xs text-mx-red">${this.escapeHtml(preview.error)}</div>`;
            } else {
                // Fallback to JSON args
                detailsHtml += `<pre class="mt-1 text-[11px] text-dark-400 overflow-x-auto whitespace-pre-wrap">${this.escapeHtml(JSON.stringify(args, null, 2))}</pre>`;
            }

            detailsHtml += `</div>`;
        }

        approvalEl.innerHTML = `
            <div class="max-w-[90%]">
                <div class="bg-mx-yellow/10 border border-mx-yellow/30 rounded-lg px-3 py-2">
                    <div class="flex items-center gap-2 mb-2">
                        <svg class="w-4 h-4 text-mx-yellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                        </svg>
                        <span class="text-sm font-medium text-mx-yellow">${_t('webui.approval_required')}</span>
                        <span class="text-[10px] text-dark-500 ml-auto">${_t('webui.approval_hint')}</span>
                    </div>
                    <div class="max-h-64 overflow-y-auto mb-3">
                        ${detailsHtml}
                    </div>
                    <div class="approval-buttons">
                        <button class="approve-btn flex-1 px-3 py-1.5 bg-mx-green/80 hover:bg-mx-green text-white text-sm rounded transition-colors" aria-label="${_t('webui.approve_btn')}">
                            ${_t('webui.approve_btn')}
                        </button>
                        <button class="reject-btn flex-1 px-3 py-1.5 bg-mx-red/80 hover:bg-mx-red text-white text-sm rounded transition-colors" aria-label="${_t('webui.reject_btn')}">
                            ${_t('webui.reject_btn')}
                        </button>
                        <button class="approve-all-btn px-3 py-1.5 bg-vscode-active hover:bg-vscode-border text-vscode-text text-sm rounded transition-colors" title="${_t('webui.approve_all_title')}" aria-label="${_t('webui.approve_all_btn')}">
                            ${_t('webui.approve_all_btn')}
                        </button>
                    </div>
                </div>
            </div>
        `;

        // Bind button events
        approvalEl.querySelector('.approve-btn').addEventListener('click', () => {
            this.handleApproval('approve');
            this.markApprovalHandled(approvalEl, _t('webui.approved'), 'mx-green');
        });
        approvalEl.querySelector('.reject-btn').addEventListener('click', () => {
            this.handleApproval('reject');
            this.markApprovalHandled(approvalEl, _t('webui.rejected'), 'mx-red');
        });
        approvalEl.querySelector('.approve-all-btn').addEventListener('click', () => {
            this.handleApproval('auto_approve_all');
            this.markApprovalHandled(approvalEl, _t('webui.all_approved'), 'mx-blue');
        });

        this.messagesEl.appendChild(approvalEl);
        this.scrollToBottom();

        // W09: Programmatically move focus to first button
        const firstBtn = approvalEl.querySelector('.approve-btn');
        if (firstBtn) firstBtn.focus();
    }

    markApprovalHandled(approvalEl, status, color) {
        // E09: Use dedicated .approval-buttons class instead of brittle CSS selector
        const buttonsDiv = approvalEl.querySelector('.approval-buttons');
        if (buttonsDiv) {
            // E09: Use inline style instead of dynamic Tailwind class text-${color}-400
            const colorMap = {
                'mx-green': '#4ade80',
                'mx-red': '#f87171',
                'mx-blue': '#60a5fa'
            };
            const hexColor = colorMap[color] || '#abb2bf';
            buttonsDiv.innerHTML = `<span class="text-xs" style="color: ${hexColor}">&#10003; ${status}</span>`;
        }
    }

    handleApproval(decision) {
        if (!this.pendingApproval) return;

        // W03/W04: use safeSend
        if (!this.safeSend({
            type: 'approval.respond',
            data: {
                interrupt_id: this.pendingApproval.interrupt_id,
                decision: { type: decision }
            }
        })) {
            return;
        }

        this.pendingApproval = null;
        this.showTypingIndicator();
    }

    // Interview (E04: duplicate check, W03/W04: safeSend)
    showInterviewRequest(data) {
        this.hideTypingIndicator();
        const interviewId = data.interview_id;
        const questions = data.questions || [];
        const skippable = data.skippable !== false;

        // E04: Prevent duplicate interview elements on reconnect
        const existingInterview = document.getElementById(`interview-${interviewId}`);
        if (existingInterview) existingInterview.remove();

        const interviewEl = document.createElement('div');
        interviewEl.className = 'interview-request message mb-3';
        interviewEl.id = `interview-${interviewId}`;

        // Track state
        const state = {
            currentTab: 0,
            answers: {},
            customTextMode: false,
            customTextBuffer: '',
        };

        const renderInterview = () => {
            const totalTabs = questions.length + 1; // +1 for submit tab
            const isSubmitTab = state.currentTab >= questions.length;

            // Build tab bar
            let tabsHtml = '';
            questions.forEach((q, i) => {
                const header = q.header || `Q${i + 1}`;
                const marker = state.answers[i] !== undefined ? '■' : (i === state.currentTab ? '▶' : '□');
                const active = i === state.currentTab ? 'bg-vscode-active text-white font-bold' : 'text-dark-400 hover:text-dark-200';
                tabsHtml += `<button class="interview-tab px-2 py-1 text-xs rounded ${active}" data-tab="${i}">${marker} ${this.escapeHtml(header)}</button>`;
            });
            const submitActive = isSubmitTab ? 'bg-vscode-active text-white font-bold' : 'text-dark-400 hover:text-dark-200';
            tabsHtml += `<button class="interview-tab px-2 py-1 text-xs rounded ${submitActive}" data-tab="${questions.length}">✔ Submit</button>`;

            // Build content
            let contentHtml = '';
            if (isSubmitTab) {
                contentHtml = `<div class="text-sm font-medium mb-2">${_t('webui.interview_submit_prompt')}</div>`;
                questions.forEach((q, i) => {
                    const header = q.header || `Q${i + 1}`;
                    const answer = state.answers[i] !== undefined ? (Array.isArray(state.answers[i]) ? state.answers[i].join(', ') : state.answers[i]) : _t('webui.interview_unanswered');
                    contentHtml += `<div class="text-xs mb-1"><span class="font-medium text-vscode-accent">${this.escapeHtml(header)}</span>: ${this.escapeHtml(String(answer))}</div>`;
                });
                contentHtml += `<div class="flex gap-2 mt-3">
                    <button class="interview-submit flex-1 px-3 py-1.5 bg-mx-green/80 hover:bg-mx-green text-white text-sm rounded transition-colors">${_t('webui.interview_submit')}</button>
                    ${skippable ? `<button class="interview-cancel px-3 py-1.5 bg-mx-red/80 hover:bg-mx-red text-white text-sm rounded transition-colors">${_t('webui.interview_cancel')}</button>` : ''}
                </div>`;
            } else {
                const q = questions[state.currentTab];
                const isMulti = q.multi_select || false;
                const options = q.options || [];

                contentHtml = `<div class="text-sm font-medium mb-3">${this.escapeHtml(q.question)}</div>`;
                contentHtml += `<div class="space-y-1">`;

                options.forEach((opt, i) => {
                    const isSelected = isMulti
                        ? (Array.isArray(state.answers[state.currentTab]) && state.answers[state.currentTab].includes(opt.label))
                        : state.answers[state.currentTab] === opt.label;
                    const inputType = isMulti ? 'checkbox' : 'radio';
                    const checked = isSelected ? 'checked' : '';
                    const selectedClass = isSelected ? 'bg-vscode-active/30 border-vscode-accent' : 'border-vscode-border hover:border-vscode-accent/50';

                    contentHtml += `
                        <label class="interview-option flex items-start gap-2 p-2 rounded border cursor-pointer transition-colors ${selectedClass}" data-option="${i}">
                            <input type="${inputType}" name="interview-q${state.currentTab}" ${checked} class="mt-0.5 accent-[#007acc]">
                            <div class="flex-1 min-w-0">
                                <div class="text-sm">${this.escapeHtml(opt.label)}</div>
                                ${opt.description ? `<div class="text-xs text-dark-400 mt-0.5">${this.escapeHtml(opt.description)}</div>` : ''}
                            </div>
                        </label>`;
                });

                // Free text option
                contentHtml += `
                    <label class="interview-option flex items-start gap-2 p-2 rounded border cursor-pointer transition-colors border-vscode-border hover:border-vscode-accent/50" data-option="custom">
                        <input type="${isMulti ? 'checkbox' : 'radio'}" name="interview-q${state.currentTab}" class="mt-0.5 accent-[#007acc]">
                        <div class="flex-1 min-w-0">
                            <div class="text-sm text-dark-400">${_t('webui.interview_custom_label')}</div>
                            <input type="text" class="interview-custom-input mt-1 w-full px-2 py-1 text-sm bg-vscode-bg border border-vscode-border rounded focus:border-vscode-accent focus:outline-none hidden" placeholder="${_t('webui.interview_answer_placeholder')}">
                        </div>
                    </label>`;

                contentHtml += `</div>`;

                // Navigation buttons
                contentHtml += `<div class="flex gap-2 mt-3">`;
                if (state.currentTab > 0) {
                    contentHtml += `<button class="interview-prev px-3 py-1 text-xs bg-vscode-active hover:bg-vscode-border text-vscode-text rounded transition-colors">${_t('webui.interview_prev')}</button>`;
                }
                contentHtml += `<div class="flex-1"></div>`;
                if (skippable) {
                    contentHtml += `<button class="interview-cancel px-3 py-1 text-xs text-dark-400 hover:text-dark-200 transition-colors">${_t('webui.interview_skip')}</button>`;
                }
                contentHtml += `<button class="interview-next px-3 py-1 text-xs bg-vscode-accent hover:bg-vscode-accent/80 text-white rounded transition-colors">${_t('webui.interview_next')}</button>`;
                contentHtml += `</div>`;
            }

            interviewEl.innerHTML = `
                <div class="max-w-[90%]">
                    <div class="bg-vscode-accent/5 border border-vscode-accent/30 rounded-lg px-3 py-2">
                        <div class="flex items-center gap-2 mb-2">
                            <svg class="w-4 h-4 text-vscode-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                            </svg>
                            <span class="text-sm font-medium text-vscode-accent">${_t('webui.interview_title')}</span>
                            <span class="text-xs text-dark-500 ml-auto">${state.currentTab + 1}/${questions.length}</span>
                        </div>
                        <div class="flex gap-1 mb-2 flex-wrap">${tabsHtml}</div>
                        <div class="border-t border-vscode-border/30 pt-2">${contentHtml}</div>
                    </div>
                </div>
            `;

            // Bind events
            interviewEl.querySelectorAll('.interview-tab').forEach(tab => {
                tab.addEventListener('click', () => {
                    state.currentTab = parseInt(tab.dataset.tab);
                    renderInterview();
                });
            });

            if (!isSubmitTab) {
                interviewEl.querySelectorAll('.interview-option').forEach(optEl => {
                    optEl.addEventListener('click', (e) => {
                        const q = questions[state.currentTab];
                        const isMulti = q.multi_select || false;
                        const optIdx = optEl.dataset.option;

                        if (optIdx === 'custom') {
                            const customInput = optEl.querySelector('.interview-custom-input');
                            if (customInput) {
                                customInput.classList.remove('hidden');
                                customInput.focus();
                                customInput.addEventListener('keydown', (ke) => {
                                    if (ke.key === 'Enter' && customInput.value.trim()) {
                                        state.answers[state.currentTab] = customInput.value.trim();
                                        state.currentTab = Math.min(state.currentTab + 1, questions.length);
                                        renderInterview();
                                    }
                                });
                            }
                            return;
                        }

                        const optionLabel = q.options[parseInt(optIdx)].label;
                        if (isMulti) {
                            const current = Array.isArray(state.answers[state.currentTab]) ? [...state.answers[state.currentTab]] : [];
                            const idx = current.indexOf(optionLabel);
                            if (idx >= 0) current.splice(idx, 1);
                            else current.push(optionLabel);
                            state.answers[state.currentTab] = current;
                            renderInterview();
                        } else {
                            state.answers[state.currentTab] = optionLabel;
                            // Auto-advance to next tab
                            state.currentTab = Math.min(state.currentTab + 1, questions.length);
                            renderInterview();
                        }
                    });
                });

                const prevBtn = interviewEl.querySelector('.interview-prev');
                if (prevBtn) {
                    prevBtn.addEventListener('click', () => {
                        state.currentTab = Math.max(0, state.currentTab - 1);
                        renderInterview();
                    });
                }

                const nextBtn = interviewEl.querySelector('.interview-next');
                if (nextBtn) {
                    nextBtn.addEventListener('click', () => {
                        state.currentTab = Math.min(state.currentTab + 1, questions.length);
                        renderInterview();
                    });
                }
            }

            // Submit button
            const submitBtn = interviewEl.querySelector('.interview-submit');
            if (submitBtn) {
                submitBtn.addEventListener('click', () => {
                    const result = {};
                    questions.forEach((q, i) => {
                        const header = q.header || `Q${i + 1}`;
                        result[header] = state.answers[i] !== undefined ? state.answers[i] : _t('webui.interview_unanswered');
                    });
                    // W03/W04: use safeSend
                    if (!this.safeSend({
                        type: 'interview.answer',
                        data: { interview_id: interviewId, answers: result }
                    })) {
                        return;
                    }
                    // Mark as completed
                    interviewEl.innerHTML = `
                        <div class="max-w-[90%]">
                            <div class="bg-mx-green/5 border border-mx-green/30 rounded-lg px-3 py-2">
                                <span class="text-xs text-mx-green">&#10003; ${_t('webui.interview_submitted')}</span>
                            </div>
                        </div>`;
                    this.showTypingIndicator();
                });
            }

            // Cancel buttons
            interviewEl.querySelectorAll('.interview-cancel').forEach(btn => {
                btn.addEventListener('click', () => {
                    // W03/W04: use safeSend
                    if (!this.safeSend({
                        type: 'interview.cancel',
                        data: { interview_id: interviewId }
                    })) {
                        return;
                    }
                    interviewEl.innerHTML = `
                        <div class="max-w-[90%]">
                            <div class="bg-mx-red/5 border border-mx-red/30 rounded-lg px-3 py-2">
                                <span class="text-xs text-mx-red">&#10007; ${_t('webui.interview_skipped')}</span>
                            </div>
                        </div>`;
                    this.showTypingIndicator();
                });
            });
        };

        renderInterview();
        this.messagesEl.appendChild(interviewEl);
        this.scrollToBottom();
    }

    // Files
    async loadFiles() {
        if (!this.sessionId) return;

        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files?depth=3&show_hidden=${this.showHiddenFiles}`);
            const data = await response.json();

            // Set file tree project name from root path (chat header stays as IEUM)
            if (data.root) {
                const folderName = data.root.split('/').pop() || data.root.split('\\').pop() || 'Project';
                const fileTreeProjectNameEl = document.getElementById('fileTreeProjectName');
                if (fileTreeProjectNameEl) {
                    fileTreeProjectNameEl.textContent = folderName.toUpperCase();
                }
            }

            // API returns { root, tree } structure
            const files = data.tree || data;
            if (Array.isArray(files)) {
                this.fileTreeData = files; // Store for @ file suggestions
                this.renderFileTree(files);
            } else {
                console.log('No files found or invalid response:', data);
                this.fileTree.innerHTML = `<div class="px-4 py-2 text-xs text-dark-500">${_t('webui.file_no_content')}</div>`;
            }
        } catch (error) {
            console.error('Failed to load files:', error);
            this.fileTree.innerHTML = `<div class="px-4 py-2 text-xs text-dark-500">${_t('webui.file_load_tree_failed')}</div>`;
        }
    }

    renderFileTree(files, container = null, level = 0) {
        const parent = container || this.fileTree;
        if (!container) parent.innerHTML = '';

        files.forEach(file => {
            const itemEl = document.createElement('div');

            if (file.type === 'directory') {
                itemEl.innerHTML = `
                    <div class="file-tree-item group flex items-center gap-1 px-2 py-1 cursor-pointer drop-target" style="padding-left: ${8 + level * 12}px" data-path="${this.escapeHtml(file.path)}" data-type="directory" draggable="true">
                        <svg class="w-4 h-4 text-dark-500 transform transition-transform folder-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
                        </svg>
                        <svg class="w-4 h-4 text-mx-yellow" fill="currentColor" viewBox="0 0 20 20">
                            <path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd"/>
                            <path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z"/>
                        </svg>
                        <span class="text-sm truncate flex-1">${this.escapeHtml(file.name)}</span>
                        <div class="folder-actions hidden group-hover:flex items-center gap-0.5">
                            <button class="folder-rename-btn p-0.5 hover:bg-vscode-border rounded text-dark-500 hover:text-white" data-tooltip="${_t('webui.rename_tooltip')}">
                                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
                                </svg>
                            </button>
                            <button class="folder-delete-btn p-0.5 hover:bg-red-500/30 rounded text-dark-500 hover:text-red-400" data-tooltip="${_t('webui.delete_tooltip')}">
                                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
                                </svg>
                            </button>
                        </div>
                    </div>
                    <div class="folder-children hidden"></div>
                `;

                const header = itemEl.querySelector('.file-tree-item');
                const children = itemEl.querySelector('.folder-children');
                const chevron = itemEl.querySelector('.folder-chevron');

                header.addEventListener('click', (e) => {
                    if (!e.target.closest('.folder-actions')) {
                        children.classList.toggle('hidden');
                        chevron.classList.toggle('rotate-90');
                        if (!children.classList.contains('hidden') && children.children.length === 0 && file.children) {
                            this.renderFileTree(file.children, children, level + 1);
                        }
                    }
                });

                header.querySelector('.folder-rename-btn').addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.renameFile(file.path, file.name);
                });

                header.querySelector('.folder-delete-btn').addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.deleteFolder(file.path, file.name);
                });

                // Drag and drop for folders
                header.addEventListener('dragstart', (e) => {
                    e.dataTransfer.setData('text/plain', JSON.stringify({ path: file.path, name: file.name, type: 'directory' }));
                    e.dataTransfer.effectAllowed = 'move';
                    header.classList.add('opacity-50');
                });

                header.addEventListener('dragend', () => {
                    header.classList.remove('opacity-50');
                });

                header.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    header.classList.add('bg-vscode-accent/30', 'border', 'border-vscode-accent');
                });

                header.addEventListener('dragleave', () => {
                    header.classList.remove('bg-vscode-accent/30', 'border', 'border-vscode-accent');
                });

                header.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    header.classList.remove('bg-vscode-accent/30', 'border', 'border-vscode-accent');

                    try {
                        const data = JSON.parse(e.dataTransfer.getData('text/plain'));
                        if (data.path !== file.path && !file.path.startsWith(data.path + '/')) {
                            await this.moveFile(data.path, data.name, file.path);
                        }
                    } catch (err) {
                        console.error('Drop error:', err);
                    }
                });
            } else {
                const ext = file.name.split('.').pop()?.toLowerCase() || '';
                const iconColors = {
                    'js': 'text-mx-yellow', 'ts': 'text-mx-blue', 'tsx': 'text-mx-blue',
                    'py': 'text-mx-green', 'html': 'text-mx-orange', 'css': 'text-mx-blue',
                    'json': 'text-mx-yellow', 'md': 'text-vscode-muted',
                };
                const iconColor = iconColors[ext] || 'text-vscode-muted';

                itemEl.innerHTML = `
                    <div class="file-tree-item group flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-vscode-active" style="padding-left: ${20 + level * 12}px" data-path="${this.escapeHtml(file.path)}" data-type="file" draggable="true">
                        <svg class="w-4 h-4 flex-shrink-0 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                        </svg>
                        <span class="text-sm truncate flex-1">${this.escapeHtml(file.name)}</span>
                        <div class="file-actions hidden group-hover:flex items-center gap-0.5">
                            <button class="rename-btn p-0.5 hover:bg-vscode-border rounded text-dark-500 hover:text-white" data-tooltip="${_t('webui.rename_tooltip')}">
                                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
                                </svg>
                            </button>
                            <button class="delete-btn p-0.5 hover:bg-red-500/30 rounded text-dark-500 hover:text-red-400" data-tooltip="${_t('webui.delete_tooltip')}">
                                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
                                </svg>
                            </button>
                        </div>
                    </div>
                `;

                const fileItem = itemEl.querySelector('.file-tree-item');
                fileItem.addEventListener('click', (e) => {
                    if (!e.target.closest('.file-actions')) {
                        this.openFile(file.path);
                    }
                });

                fileItem.querySelector('.rename-btn').addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.renameFile(file.path, file.name);
                });

                fileItem.querySelector('.delete-btn').addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.deleteFile(file.path, file.name);
                });

                // Drag for files
                fileItem.addEventListener('dragstart', (e) => {
                    e.dataTransfer.setData('text/plain', JSON.stringify({ path: file.path, name: file.name, type: 'file' }));
                    e.dataTransfer.effectAllowed = 'move';
                    fileItem.classList.add('opacity-50');
                });

                fileItem.addEventListener('dragend', () => {
                    fileItem.classList.remove('opacity-50');
                });
            }

            parent.appendChild(itemEl);
        });
    }

    handleFileChange(data) {
        this.showToast(_t('webui.file_changed', {action: data.action, path: data.path}), 'success');
        this.loadFiles();
    }

    async moveFile(sourcePath, fileName, targetFolderPath) {
        const newPath = targetFolderPath + '/' + fileName;

        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/rename`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ old_path: sourcePath, new_path: newPath })
            });

            if (response.ok) {
                this.showToast(_t('webui.file_moved', {name: fileName, target: targetFolderPath}), 'success');
                this.loadFiles();
                // Update open file if moved
                if (this.openFiles.has(sourcePath)) {
                    const fileData = this.openFiles.get(sourcePath);
                    this.openFiles.delete(sourcePath);
                    this.openFiles.set(newPath, fileData);
                    if (this.activeFile === sourcePath) {
                        this.activeFile = newPath;
                    }
                    this.updateTabs();
                }
            } else {
                const data = await response.json();
                this.showToast(data.detail || _t('webui.move_failed'), 'error');
            }
        } catch (error) {
            this.showToast(_t('webui.move_failed'), 'error');
        }
    }

    async renameFile(path, oldName) {
        const newName = prompt(_t('webui.enter_new_name'), oldName);
        if (!newName || newName === oldName) return;

        const parentPath = path.substring(0, path.lastIndexOf('/'));
        const newPath = parentPath + '/' + newName;

        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/rename`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ old_path: path, new_path: newPath })
            });

            if (response.ok) {
                this.showToast(_t('webui.renamed', {old: oldName, new: newName}), 'success');
                this.loadFiles();
                // Update open file if renamed
                if (this.openFiles.has(path)) {
                    const fileData = this.openFiles.get(path);
                    this.openFiles.delete(path);
                    this.openFiles.set(newPath, fileData);
                    if (this.activeFile === path) {
                        this.activeFile = newPath;
                    }
                    this.updateTabs();
                }
            } else {
                const data = await response.json();
                this.showToast(data.detail || _t('webui.rename_failed'), 'error');
            }
        } catch (error) {
            this.showToast(_t('webui.rename_failed'), 'error');
        }
    }

    async deleteFile(path, name) {
        if (!confirm(_t('webui.confirm_delete_file', {name: name}))) return;

        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/delete`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ path })
            });

            if (response.ok) {
                this.showToast(_t('webui.deleted', {name: name}), 'success');
                this.loadFiles();
                // Close file if open
                if (this.openFiles.has(path)) {
                    this.closeFile(path);
                }
            } else {
                const data = await response.json();
                this.showToast(data.detail || _t('webui.delete_failed'), 'error');
            }
        } catch (error) {
            this.showToast(_t('webui.delete_failed'), 'error');
        }
    }

    deleteFolder(path, name) {
        const modal = document.createElement('div');
        modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
        modal.innerHTML = `
            <div class="bg-vscode-sidebar border border-vscode-border rounded-lg shadow-2xl max-w-md w-full mx-4">
                <div class="p-4 border-b border-vscode-border flex items-center gap-2">
                    <svg class="w-5 h-5 text-mx-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                    </svg>
                    <h3 class="font-medium text-white">${_t('webui.folder_delete_warning')}</h3>
                </div>
                <div class="p-4">
                    <p class="text-dark-300 text-sm">${_t('webui.folder_delete_confirm', {name: `<span class="text-mx-yellow font-medium">${name}</span>`})}</p>
                    <p class="text-mx-red text-xs mt-2 font-medium">${_t('webui.folder_delete_all_warning')}</p>
                </div>
                <div class="p-4 border-t border-vscode-border flex items-center justify-end gap-3">
                    <button class="cancel-btn px-4 py-2 rounded border border-vscode-border text-dark-300 hover:text-white hover:border-dark-500 transition-colors">
                        ${_t('webui.cancel_btn')}
                    </button>
                    <button class="delete-btn px-4 py-2 rounded bg-mx-red text-white hover:bg-red-600 transition-colors">
                        ${_t('webui.delete_btn')}
                    </button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        modal.querySelector('.cancel-btn').addEventListener('click', () => {
            modal.remove();
        });

        modal.querySelector('.delete-btn').addEventListener('click', async () => {
            modal.remove();
            await this.performDeleteFolder(path, name);
        });

        modal.addEventListener('click', (e) => {
            if (e.target === modal) modal.remove();
        });
    }

    async performDeleteFolder(path, name) {
        try {
            const response = await fetch(`/api/sessions/${this.sessionId}/files/delete`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ path })
            });

            if (response.ok) {
                this.showToast(_t('webui.folder_deleted', {name: name}), 'success');
                this.loadFiles();
                // Close any open files from this folder
                for (const openPath of this.openFiles.keys()) {
                    if (openPath.startsWith(path)) {
                        this.closeFile(openPath);
                    }
                }
            } else {
                const data = await response.json();
                this.showToast(data.detail || _t('webui.folder_delete_failed'), 'error');
            }
        } catch (error) {
            this.showToast(_t('webui.folder_delete_failed'), 'error');
        }
    }

    // UI Helpers
    clearMessages() {
        this.messagesEl.innerHTML = '';
        this.showWelcomeMessage(); // W07
    }

    scrollToBottom() {
        this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
    }

    showToast(message, type = 'default') {
        const toast = document.createElement('div');
        toast.className = `px-4 py-2 rounded bg-vscode-sidebar border text-sm ${
            type === 'success' ? 'border-mx-green/50 text-mx-green' :
            type === 'error' ? 'border-mx-red/50 text-mx-red' :
            'border-vscode-border text-vscode-text'
        }`;
        toast.textContent = message;
        this.toastContainer.appendChild(toast);

        setTimeout(() => {
            toast.style.opacity = '0';
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }

    // Settings
    showSettingsModal() {
        this.populateSettingsForm();
        this.settingsModal.classList.remove('hidden');
        this.settingsModal.classList.add('flex');
        this.onProviderChange();
    }

    hideSettingsModal() {
        this.settingsModal.classList.add('hidden');
        this.settingsModal.classList.remove('flex');
    }

    async loadSettings() {
        try {
            const response = await fetch('/api/sessions/settings/global');
            if (response.ok) {
                const serverSettings = await response.json();
                if (serverSettings.provider) {
                    this.modelSettings = {
                        provider: serverSettings.provider || 'ollama',
                        baseUrl: serverSettings.base_url || '',
                        apiKey: serverSettings.api_key || '',
                        model: serverSettings.model || '',
                        githubPat: serverSettings.github_pat || '',
                        githubHost: serverSettings.github_host || ''
                    };
                    localStorage.setItem('ieum-model-settings', JSON.stringify(this.modelSettings));
                    this.updateModelDisplay();
                    return;
                }
            }
        } catch (e) {
            console.log('No server settings found');
        }

        const saved = localStorage.getItem('ieum-model-settings');
        if (saved) {
            try {
                this.modelSettings = JSON.parse(saved);
            } catch (e) {}
        }
        this.updateModelDisplay();
    }

    async saveSettings() {
        this.modelSettings.provider = this.providerSelect?.value || 'ollama';
        this.modelSettings.baseUrl = this.baseUrlInput?.value || '';
        this.modelSettings.apiKey = this.apiKeyInput?.value || '';
        this.modelSettings.githubPat = document.getElementById('githubPatInput')?.value || '';
        this.modelSettings.githubHost = document.getElementById('githubHostInput')?.value || '';

        const isLocalProvider = ['ollama', 'vllm', 'lmstudio'].includes(this.modelSettings.provider);
        this.modelSettings.model = isLocalProvider
            ? this.modelSelect?.value || ''
            : this.modelInput?.value || '';

        const saveLocation = document.getElementById('saveLocationSelect')?.value || 'global';

        localStorage.setItem('ieum-model-settings', JSON.stringify(this.modelSettings));

        try {
            const response = await fetch('/api/sessions/settings/global', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    provider: this.modelSettings.provider,
                    base_url: this.modelSettings.baseUrl || null,
                    api_key: this.modelSettings.apiKey || null,
                    model: this.modelSettings.model,
                    github_pat: this.modelSettings.githubPat || null,
                    github_host: this.modelSettings.githubHost || null,
                    save_to: saveLocation
                })
            });
            const result = await response.json();
            const locationLabel = saveLocation === 'global' ? _t('webui.settings_location_global') : _t('webui.settings_location_local');
            this.showToast(_t('webui.settings_saved', {location: locationLabel}), 'success');
        } catch (error) {
            this.showToast(_t('webui.settings_save_failed'), 'error');
        }

        this.updateModelDisplay();
        this.hideSettingsModal();

        // Handle language change (must be after modal is hidden)
        const languageSelect = document.getElementById('languageSelect');
        if (languageSelect) {
            const newLang = languageSelect.value;
            const currentLang = localStorage.getItem('ieum_lang') || (navigator.language?.startsWith('ko') ? 'ko' : 'en');
            if (newLang !== currentLang) {
                localStorage.setItem('ieum_lang', newLang);
                this.showToast(_t('webui.language_changed'), 'success');
                setTimeout(() => window.location.reload(), 800);
            }
        }
    }

    populateSettingsForm() {
        if (this.providerSelect) this.providerSelect.value = this.modelSettings.provider;
        if (this.baseUrlInput) this.baseUrlInput.value = this.modelSettings.baseUrl;
        if (this.apiKeyInput) this.apiKeyInput.value = this.modelSettings.apiKey;

        // Language setting
        const languageSelect = document.getElementById('languageSelect');
        if (languageSelect) {
            languageSelect.value = localStorage.getItem('ieum_lang') || (navigator.language?.startsWith('ko') ? 'ko' : 'en');
        }

        // GitHub settings
        const githubPatInput = document.getElementById('githubPatInput');
        const githubHostInput = document.getElementById('githubHostInput');
        if (githubPatInput) githubPatInput.value = this.modelSettings.githubPat || '';
        if (githubHostInput) githubHostInput.value = this.modelSettings.githubHost || '';

        const isLocalProvider = ['ollama', 'vllm', 'lmstudio'].includes(this.modelSettings.provider);
        if (isLocalProvider && this.modelSelect) {
            this.modelSelect.value = this.modelSettings.model;
        } else if (this.modelInput) {
            this.modelInput.value = this.modelSettings.model;
        }
    }

    updateModelDisplay() {
        const model = this.modelSettings.model || 'Not configured';
        if (this.modelNameEl) {
            this.modelNameEl.textContent = model;
        }
    }

    onProviderChange() {
        const provider = this.providerSelect?.value || 'ollama';
        const isLocalProvider = ['ollama', 'vllm', 'lmstudio'].includes(provider);
        const needsApiKey = ['openrouter', 'openai', 'anthropic'].includes(provider);

        if (this.baseUrlGroup) this.baseUrlGroup.classList.toggle('hidden', !isLocalProvider);
        if (this.apiKeyGroup) this.apiKeyGroup.classList.toggle('hidden', !needsApiKey);
        if (this.modelSelectWrapper) this.modelSelectWrapper.classList.toggle('hidden', !isLocalProvider);
        if (this.modelInputWrapper) this.modelInputWrapper.classList.toggle('hidden', isLocalProvider);

        if (this.baseUrlInput && isLocalProvider && !this.baseUrlInput.value) {
            const defaultUrls = { ollama: 'http://localhost:11434', vllm: 'http://localhost:8000', lmstudio: 'http://localhost:1234' };
            this.baseUrlInput.value = defaultUrls[provider] || '';
        }

        if (isLocalProvider) this.fetchModels();
    }

    async fetchModels() {
        const provider = this.providerSelect?.value;
        const baseUrl = this.baseUrlInput?.value;
        if (!provider || !baseUrl) return;

        const isLocalProvider = ['ollama', 'vllm', 'lmstudio'].includes(provider);
        if (!isLocalProvider) return;

        if (this.modelStatus) {
            this.modelStatus.textContent = _t('webui.model_loading');
            this.modelStatus.classList.remove('hidden');
        }

        try {
            const response = await fetch(`/api/models?provider=${provider}&base_url=${encodeURIComponent(baseUrl)}`);
            const data = await response.json();

            if (data.error) {
                if (this.modelStatus) this.modelStatus.textContent = data.error;
                return;
            }

            if (this.modelSelect) {
                this.modelSelect.innerHTML = `<option value="">${_t('webui.model_select_placeholder')}</option>`;
                for (const model of data.models) {
                    const option = document.createElement('option');
                    option.value = model.id;
                    option.textContent = model.name;
                    this.modelSelect.appendChild(option);
                }
                if (this.modelSettings.model) {
                    this.modelSelect.value = this.modelSettings.model;
                }
            }

            if (this.modelStatus) this.modelStatus.textContent = _t('webui.model_count', {count: data.models.length});
        } catch (error) {
            if (this.modelStatus) this.modelStatus.textContent = _t('webui.model_load_failed');
        }
    }

    toggleAutoApprove() {
        // Three-mode cycling: manual → auto(full_trusted) → plan → manual
        if (this.approvalPolicy === 'manual') {
            this.setApprovalPolicy('full_trusted');
        } else if (this.approvalPolicy === 'full_trusted') {
            this.setApprovalPolicy('plan_only');
        } else {
            this.setApprovalPolicy('manual');
        }
    }

    normalizeApprovalPolicy(policy) {
        const value = (policy || '').toString().trim().toLowerCase().replace(/-/g, '_');
        if (value === 'manual') return 'manual';
        if (value === 'read_only' || value === 'readonly') return 'read_only';
        if (value === 'safe_write') return 'safe_write';
        if (value === 'full_trusted' || value === 'trusted') return 'full_trusted';
        if (value === 'plan_only' || value === 'plan') return 'plan_only';
        return null;
    }

    applyApprovalPolicyUI() {
        this.autoApprove = this.approvalPolicy === 'full_trusted';
        const textEl = document.getElementById('autoApproveText');
        // Remove all mode classes first
        this.autoApproveIndicator?.classList.remove('translate-x-4');
        this.autoApproveToggleBtn?.classList.remove('bg-mx-red', 'bg-indigo-600');
        this.autoApproveToggleBtn?.classList.add('bg-vscode-border');
        textEl?.classList.remove('text-mx-red', 'text-indigo-400', 'font-medium');

        if (this.approvalPolicy === 'full_trusted') {
            this.autoApproveIndicator?.classList.add('translate-x-4');
            this.autoApproveToggleBtn?.classList.add('bg-mx-red');
            this.autoApproveToggleBtn?.classList.remove('bg-vscode-border');
            textEl?.classList.add('text-mx-red', 'font-medium');
            if (textEl) textEl.textContent = 'Auto';
        } else if (this.approvalPolicy === 'plan_only') {
            this.autoApproveIndicator?.classList.add('translate-x-4');
            this.autoApproveToggleBtn?.classList.add('bg-indigo-600');
            this.autoApproveToggleBtn?.classList.remove('bg-vscode-border');
            textEl?.classList.add('text-indigo-400', 'font-medium');
            if (textEl) textEl.textContent = 'Plan';
        } else {
            if (textEl) textEl.textContent = 'Manual';
        }
    }

    setApprovalPolicy(policy, options = {}) {
        const normalized = this.normalizeApprovalPolicy(policy);
        if (!normalized) return false;

        const {
            sendToServer = true,
            notify = true,
            persist = true,
        } = options;

        this.approvalPolicy = normalized;
        if (persist) {
            localStorage.setItem('ieum_approval_policy', normalized);
        }
        this.applyApprovalPolicyUI();

        if (sendToServer && this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({
                type: 'session.set_approval_policy',
                data: { approval_policy: normalized }
            }));
        }

        if (notify) {
            this.showToast(`Approval policy: ${normalized}`, 'default');
        }
        return true;
    }

    loadLastSeqForSession(sessionId) {
        if (!sessionId) return 0;
        const raw = localStorage.getItem(`ieum_last_seq_${sessionId}`);
        const parsed = Number.parseInt(raw || '0', 10);
        return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
    }

    persistLastSeqForSession() {
        if (!this.sessionId) return;
        localStorage.setItem(`ieum_last_seq_${this.sessionId}`, String(this.lastSeq || 0));
    }

    // --- W03/W04: Safe WebSocket send helper ---
    safeSend(data) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
            return true;
        }
        this.showToast(_t('webui.ws_disconnected_wait'), 'error');
        this.setInteractiveElementsEnabled(false);
        return false;
    }

    setInteractiveElementsEnabled(enabled) {
        // Disable/enable approval and interview buttons when disconnected
        const approvalBtns = this.messagesEl.querySelectorAll('.approve-btn, .reject-btn, .approve-all-btn');
        const interviewBtns = this.messagesEl.querySelectorAll('.interview-submit, .interview-cancel, .interview-next, .interview-prev');
        [...approvalBtns, ...interviewBtns].forEach(btn => {
            btn.disabled = !enabled;
            btn.style.opacity = enabled ? '' : '0.5';
            btn.style.cursor = enabled ? '' : 'not-allowed';
        });
    }

    // --- W06: Keyboard shortcuts ---
    initKeyboardShortcuts() {
        document.addEventListener('keydown', (e) => {
            const activeEl = document.activeElement;
            const isInputFocused = activeEl && (
                activeEl.tagName === 'INPUT' ||
                activeEl.tagName === 'TEXTAREA' ||
                activeEl.tagName === 'SELECT' ||
                activeEl.isContentEditable
            );

            // Escape - interrupt/cancel
            if (e.key === 'Escape' && !isInputFocused) {
                e.preventDefault();
                if (this.isStreaming) {
                    this.safeSend({ type: 'chat.cancel', data: {} });
                    this.showToast(_t('webui.interrupt_requested'), 'default');
                }
                // Close any open shortcuts modal
                const shortcutsModal = document.querySelector('.shortcuts-modal-overlay');
                if (shortcutsModal) shortcutsModal.remove();
                return;
            }

            // Ctrl/Cmd+Shift+N - New session
            if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'N') {
                e.preventDefault();
                this.createSession();
                return;
            }

            // Ctrl/Cmd+Shift+A - Toggle auto-approve
            if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'A') {
                e.preventDefault();
                this.toggleAutoApprove();
                this.showToast(this.autoApprove ? _t('webui.auto_approve_enabled') : _t('webui.auto_approve_disabled'), 'default');
                return;
            }

            // Quick approval keys (y/n/a) - only when approval pending and not typing
            if (!isInputFocused && this.pendingApproval) {
                if (e.key === 'y' || e.key === 'Y') {
                    e.preventDefault();
                    const approvalEl = this.messagesEl.querySelector('.approval-request:last-child');
                    this.handleApproval('approve');
                    if (approvalEl) this.markApprovalHandled(approvalEl, _t('webui.approved'), 'mx-green');
                    return;
                }
                if (e.key === 'n' || e.key === 'N') {
                    e.preventDefault();
                    const approvalEl = this.messagesEl.querySelector('.approval-request:last-child');
                    this.handleApproval('reject');
                    if (approvalEl) this.markApprovalHandled(approvalEl, _t('webui.rejected'), 'mx-red');
                    return;
                }
                if (e.key === 'a' || e.key === 'A') {
                    e.preventDefault();
                    const approvalEl = this.messagesEl.querySelector('.approval-request:last-child');
                    this.handleApproval('auto_approve_all');
                    if (approvalEl) this.markApprovalHandled(approvalEl, _t('webui.all_approved'), 'mx-blue');
                    return;
                }
            }

            // ? - Show keyboard shortcuts (when not typing)
            if (e.key === '?' && !isInputFocused) {
                e.preventDefault();
                this.showKeyboardShortcuts();
                return;
            }
        });
    }

    showKeyboardShortcuts() {
        // Remove existing modal if present
        const existing = document.querySelector('.shortcuts-modal-overlay');
        if (existing) { existing.remove(); return; }

        const overlay = document.createElement('div');
        overlay.className = 'shortcuts-modal-overlay';
        overlay.innerHTML = `
            <div class="shortcuts-modal">
                <h3>${_t('webui.shortcuts_title')}</h3>
                <div class="shortcut-section">${_t('webui.shortcut_general')}</div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_interrupt')}</span><kbd>Esc</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_new_session')}</span><kbd>Ctrl+Shift+N</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_auto_approve_toggle')}</span><kbd>Ctrl+Shift+A</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_this_help')}</span><kbd>?</kbd></div>
                <div class="shortcut-section">${_t('webui.shortcut_approval_pending')}</div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_approve')}</span><kbd>Y</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_reject')}</span><kbd>N</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_approve_all')}</span><kbd>A</kbd></div>
                <div class="shortcut-section">${_t('webui.shortcut_editor')}</div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_add_context')}</span><kbd>Ctrl+L</kbd></div>
                <div class="shortcut-row"><span>${_t('webui.shortcut_save_file')}</span><kbd>Ctrl+S</kbd></div>
            </div>
        `;

        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) overlay.remove();
        });

        document.body.appendChild(overlay);
    }

    // --- W07: Welcome message ---
    showWelcomeMessage() {
        if (this.messagesEl.children.length > 0) return;

        const welcomeEl = document.createElement('div');
        welcomeEl.className = 'welcome-message';
        welcomeEl.id = 'chatWelcome';
        welcomeEl.innerHTML = `
            <svg class="w-12 h-12 mb-3 text-vscode-accent opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
            </svg>
            <h2>IEUM</h2>
            <p>${_t('webui.welcome_hint')}</p>
            <p><code style="font-size: 0.75rem; background: #2c313a; padding: 0.125rem 0.375rem; border-radius: 0.25rem;">@</code> ${_t('webui.welcome_file_ref')} &nbsp; <code style="font-size: 0.75rem; background: #2c313a; padding: 0.125rem 0.375rem; border-radius: 0.25rem;">/</code> ${_t('webui.welcome_commands')}</p>
            <div class="shortcuts-hint">
                <div><span>${_t('webui.shortcut_interrupt')}</span><kbd>Esc</kbd></div>
                <div><span>${_t('webui.shortcut_new_session')}</span><kbd>Ctrl+Shift+N</kbd></div>
                <div><span>${_t('webui.approval_yna')}</span><kbd>Y</kbd> <kbd>N</kbd> <kbd>A</kbd></div>
                <div><span>${_t('webui.shortcut_help')}</span><kbd>?</kbd></div>
            </div>
        `;
        this.messagesEl.appendChild(welcomeEl);
    }

    removeWelcomeMessage() {
        const welcome = document.getElementById('chatWelcome');
        if (welcome) welcome.remove();
    }

    // --- W08: Persistent connection banner ---
    showConnectionBanner(message, showReconnect = true) {
        this.hideConnectionBanner();
        const banner = document.createElement('div');
        banner.className = 'connection-banner';
        banner.id = 'connectionBanner';
        banner.innerHTML = `
            <span>${this.escapeHtml(message)}</span>
            <div style="display: flex; align-items: center; gap: 0.5rem;">
                ${showReconnect ? `<button class="reconnect-btn">${_t('webui.reconnect_btn')}</button>` : ''}
                <button class="banner-close" aria-label="${_t('webui.close_btn')}">&times;</button>
            </div>
        `;

        const reconnectBtn = banner.querySelector('.reconnect-btn');
        if (reconnectBtn) {
            reconnectBtn.addEventListener('click', () => {
                this.hideConnectionBanner();
                this.reconnectAttempts = 0;
                this.connectWebSocket();
            });
        }

        banner.querySelector('.banner-close').addEventListener('click', () => {
            this.hideConnectionBanner();
        });

        // Insert before messages content
        this.messagesEl.parentElement.insertBefore(banner, this.messagesEl);
    }

    hideConnectionBanner() {
        const banner = document.getElementById('connectionBanner');
        if (banner) banner.remove();
    }

    // Utilities
    escapeHtml(text) {
        if (!text) return '';
        text = String(text);
        return text.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
    }

    renderDiff(diffText) {
        if (!diffText) return '';
        const lines = diffText.split('\n');
        let html = '';
        for (const line of lines) {
            let className = 'px-2 py-0.5';
            let prefix = '';
            if (line.startsWith('+++') || line.startsWith('---')) {
                className += ' text-dark-500';
            } else if (line.startsWith('@@')) {
                className += ' text-mx-purple bg-mx-purple/10';
            } else if (line.startsWith('+')) {
                className += ' text-mx-green bg-mx-green/10';
                prefix = '<span class="select-none text-mx-green/50 mr-2">+</span>';
            } else if (line.startsWith('-')) {
                className += ' text-mx-red bg-mx-red/10';
                prefix = '<span class="select-none text-mx-red/50 mr-2">-</span>';
            } else {
                className += ' text-dark-400';
                prefix = '<span class="select-none text-dark-600 mr-2"> </span>';
            }
            html += `<div class="${className}">${prefix}${this.escapeHtml(line.startsWith('+') || line.startsWith('-') ? line.slice(1) : line)}</div>`;
        }
        return html;
    }

    renderMarkdown(text) {
        const self = this;
        return text
            .replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
                const escapedCode = self.escapeHtml(code.trim());
                const safeAttr = escapedCode.replace(/"/g, '&quot;');
                return `<div class="my-2 rounded overflow-hidden border border-vscode-border">
                    <div class="bg-vscode-sidebar px-2 py-1 text-xs text-dark-400 flex justify-between">
                        <span>${self.escapeHtml(lang || 'code')}</span>
                        <button class="hover:text-white copy-code-btn" data-copy-text="${safeAttr}">${_t('webui.copy_btn')}</button>
                    </div>
                    <pre class="bg-vscode-bg p-2 overflow-x-auto text-xs"><code>${escapedCode}</code></pre>
                </div>`;
            })
            .replace(/`([^`]+)`/g, (_, content) => `<code class="bg-vscode-active px-1 rounded text-pink-400">${self.escapeHtml(content)}</code>`)
            .replace(/\*\*(.+?)\*\*/g, (_, content) => `<strong>${self.escapeHtml(content)}</strong>`)
            .replace(/\*(.+?)\*/g, (_, content) => `<em>${self.escapeHtml(content)}</em>`)
            .replace(/\n/g, '<br>');
    }
}

// Initialize app
const app = new IeumApp();
