document.addEventListener('DOMContentLoaded', async () => {
    const _pageUrl = new URL(window.location.href);
    const _sameOrigin = `${_pageUrl.protocol}//${_pageUrl.host}`;
    const _launcherOrigin = _pageUrl.port === '8001' ? _sameOrigin : 'http://127.0.0.1:8001';
    const _defaultBackendOrigin = (_pageUrl.port === '8000' || _pageUrl.port === '8001')
        ? _sameOrigin
        : 'http://127.0.0.1:8000';
    const _wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const CHAT_SHOW_FLOW_IN_THREAD = false;

    let BACKEND_URL = _defaultBackendOrigin;
    let _preferredWsHost = '127.0.0.1';
    function _setBackendUrl(origin) {
        const resolved = String(origin || '').trim().replace(/\/+$/, '');
        if (!resolved) return;
        BACKEND_URL = resolved;
        try {
            const parsed = new URL(resolved);
            if (parsed.port === '8000' && parsed.hostname) {
                _preferredWsHost = parsed.hostname;
            }
        } catch (_) {}
    }

    const _apiBootstrapModule = window.AgentFrontendModules.createApiBootstrapModule({
        pageUrl: _pageUrl,
        sameOrigin: _sameOrigin,
        launcherOrigin: _launcherOrigin,
        defaultBackendOrigin: _defaultBackendOrigin,
        wsProtocol: _wsProtocol,
        setBackendUrl: _setBackendUrl,
        getBackendUrl: () => BACKEND_URL,
        getPreferredWsHost: () => _preferredWsHost,
    });
    const LAUNCHER_URL = _apiBootstrapModule.launcherUrl;
    const _frontendClientId = _apiBootstrapModule.frontendClientId;
    const _webSocketHosts = _apiBootstrapModule.webSocketHosts;
    const _buildWebSocketUrl = _apiBootstrapModule.buildWebSocketUrl;
    const _resolveApiUrl = _apiBootstrapModule.resolveApiUrl;
    const _refreshAuthToken = _apiBootstrapModule.refreshAuthToken;
    const authFetch = _apiBootstrapModule.authFetch;
    const authFetchJson = _apiBootstrapModule.authFetchJson;
    const _getAuthToken = _apiBootstrapModule.getAuthToken;
    let _pageClosing = false;
    await _apiBootstrapModule.init();

    const modelSelect     = document.getElementById('settings-model-select');
    const fastModelSelect = document.getElementById('settings-fast-model-select');
    const fallbackModelSelect = document.getElementById('settings-fallback-model-select');
    const refreshModelsBtn = document.getElementById('refresh-models-btn');
    const goalInput       = document.getElementById('goal-input');
    const goalModeHint    = document.getElementById('goal-mode-hint');
    const copilotEnabledSettingsToggle = document.getElementById('copilot-enabled-settings-toggle');
    const copilotMorningBriefToggle = document.getElementById('copilot-morning-brief-toggle');
    const copilotBriefingTimeInput = document.getElementById('copilot-briefing-time');
    const copilotFollowupToggle = document.getElementById('copilot-followup-toggle');
    const copilotFollowupTimeInput = document.getElementById('copilot-followup-time');
    const copilotAutonomyLevelInput = document.getElementById('copilot-autonomy-level');
    const copilotMorningInstructionsInput = document.getElementById('copilot-morning-instructions');
    const copilotFollowupInstructionsInput = document.getElementById('copilot-followup-instructions');
    const copilotQuickstartSelect = document.getElementById('copilot-quickstart-select');
    const copilotQuickstartBtn = document.getElementById('copilot-quickstart-btn');
    const copilotRunNowBtn = document.getElementById('copilot-run-now-btn');
    const copilotFollowupRunBtn = document.getElementById('copilot-followup-run-btn');
    const copilotMorningStatusNote = document.getElementById('copilot-morning-status-note');
    const copilotFollowupStatusNote = document.getElementById('copilot-followup-status-note');
    const contextInput    = document.getElementById('context-input');
    const fileInput       = document.getElementById('file-input');
    const fileLabel       = document.getElementById('file-label');
    const folderPathInput  = document.getElementById('folder-path-input');
    const folderBrowseBtn  = document.getElementById('folder-browse-btn');
    const folderClearBtn   = document.getElementById('folder-clear-btn');
    const composerSendBtn = document.getElementById('composer-send-btn');
    const schedulePickerBtn = document.getElementById('schedule-picker-btn');
    const schedulePickerLabel = document.getElementById('schedule-picker-label');
    const schedulePickerDropdown = document.getElementById('schedule-picker-dropdown');
    const scheduleIntervalRow = document.getElementById('schedule-interval-row');
    const scheduleFirstRunRow = document.getElementById('schedule-first-run-row');
    const scheduleFirstRunInput = document.getElementById('schedule-first-run');
    const scheduleIntervalInput = document.getElementById('schedule-interval');
    const scheduleUnit = document.getElementById('schedule-unit');
    const schedCheckOnce = document.getElementById('sched-check-once');
    const schedCheckRepeat = document.getElementById('sched-check-repeat');
    const modelWarning        = document.getElementById('model-warning');
    const tabs                = document.querySelectorAll('.tab');
    const tokenCount          = document.getElementById('token-count');
    const budgetCount         = document.getElementById('budget-count');
    const tokenCounterEl      = document.getElementById('token-counter');
    const budgetCounterEl     = document.getElementById('budget-counter');
    const approvalOverlay     = document.getElementById('approval-overlay');
    const approvalStepNum     = document.getElementById('approval-step-num');
    const approvalStepDesc    = document.getElementById('approval-step-desc');
    const approvalNote        = document.getElementById('approval-note');
    const approvalApproveBtn  = document.getElementById('approval-approve-btn');
    const approvalAlwaysBtn    = document.getElementById('approval-approve-always-btn');
    const approvalRejectBtn   = document.getElementById('approval-reject-btn');

    // Settings modal refs
    const settingsBtn       = document.getElementById('settings-btn');
    const settingsOverlay   = document.getElementById('settings-overlay');
    const settingsCloseBtn  = document.getElementById('settings-close-btn');
    const settingsSaveBtn   = document.getElementById('settings-save-btn');
    const settingsCancelBtn = document.getElementById('settings-cancel-btn');
    const outlookToggle     = document.getElementById('outlook-toggle');
    const nvidiaApiKeyInput = document.getElementById('nvidia-api-key');
    const braveApiKeyInput  = document.getElementById('brave-api-key');
    const ollamaCloudApiKeyInput = document.getElementById('ollama-cloud-api-key');
    const openaiApiKeyInput = document.getElementById('openai-api-key');
    const geminiApiKeyInput = document.getElementById('gemini-api-key');
    const ollamaCloudApiKeyTestBtn = document.getElementById('ollama-cloud-api-key-test-btn');
    const openaiApiKeyTestBtn = document.getElementById('openai-api-key-test-btn');
    const geminiApiKeyTestBtn = document.getElementById('gemini-api-key-test-btn');
    const nvidiaApiKeyTestBtn = document.getElementById('nvidia-api-key-test-btn');
    const ollamaCloudApiKeyTestStatus = document.getElementById('ollama-cloud-api-key-test-status');
    const openaiApiKeyTestStatus = document.getElementById('openai-api-key-test-status');
    const geminiApiKeyTestStatus = document.getElementById('gemini-api-key-test-status');
    const nvidiaApiKeyTestStatus = document.getElementById('nvidia-api-key-test-status');
    const llamacppUrlInput      = document.getElementById('llamacpp-url');
    const llamacppModelPathInput = document.getElementById('llamacpp-model-path');
    const llamacppExtraArgsInput = document.getElementById('llamacpp-extra-args');
    // llamacpp-start-btn removed — server auto-starts on model use
    const llamacppStopBtn        = document.getElementById('llamacpp-stop-btn');
    const llamacppRunStatus      = document.getElementById('llamacpp-run-status');
    const llamacppDetectBtn      = document.getElementById('llamacpp-detect-btn');
    const llamacppOllamaModelsSel = document.getElementById('llamacpp-ollama-models');
    const llamacppInstallBtn     = document.getElementById('llamacpp-install-btn');
    const ollamaApplyRecommendedBtn = document.getElementById('ollama-apply-recommended-btn');
    const ollamaPullRecommendedBtn = document.getElementById('ollama-pull-recommended-btn');
    const ollamaCloudNote = document.getElementById('ollama-cloud-note');
    const ollamaApiNote    = document.getElementById('ollama-api-note');
    const nvidiaNote        = document.getElementById('nvidia-note');
    const braveNote         = document.getElementById('brave-note');
    const openaiNote        = document.getElementById('openai-note');
    const geminiNote        = document.getElementById('gemini-note');
    const outlookNote       = document.getElementById('outlook-note');
    const outlookBadge      = document.getElementById('outlook-support-badge');
    const outlookTestBtn    = document.getElementById('outlook-test-btn');
    const outlookTestResult = document.getElementById('outlook-test-result');
    const outlookFolderSection = document.getElementById('outlook-folder-section');
    const outlookScanBtn    = document.getElementById('outlook-scan-folders-btn');
    const outlookContextStatus = document.getElementById('outlook-context-status');
    const outlookFolderTree = document.getElementById('outlook-folder-tree');
    const outlookSelectedDiv = document.getElementById('outlook-selected-folders');
    const outlookContextCount = document.getElementById('outlook-context-count');
    const googleFolderSection = document.getElementById('google-folder-section');
    const googleScanBtn     = document.getElementById('google-scan-folders-btn');
    const googleContextStatus = document.getElementById('google-context-status');
    const googleFolderTree  = document.getElementById('google-folder-tree');
    const googleSelectedDiv = document.getElementById('google-selected-folders');
    const onedriveModeLocal = document.getElementById('onedrive-mode-local');
    const onedriveModeGraph = document.getElementById('onedrive-mode-graph');
    const onedriveLocalSettings = document.getElementById('onedrive-local-settings');
    const onedriveGraphSettings = document.getElementById('onedrive-graph-settings');
    const onedriveLocalPath = document.getElementById('onedrive-local-path');
    const onedriveDetectBtn = document.getElementById('onedrive-detect-btn');
    const onedriveLocalStatus = document.getElementById('onedrive-local-status');
    const onedriveContextHint = document.getElementById('onedrive-context-hint');
    const googleDriveContextHint = document.getElementById('google-drive-context-hint');
    const googleContextCount = document.getElementById('google-context-count');
    const filesystemContextCount = document.getElementById('filesystem-context-count');
    const filesystemContextRow = document.getElementById('filesystem-context-row');
    const filesystemContextPath = document.getElementById('filesystem-context-path');
    const filesystemContextDescription = document.getElementById('filesystem-context-description');
    const filesystemContextBrowseBtn = document.getElementById('filesystem-context-browse-btn');
    const filesystemContextAddBtn = document.getElementById('filesystem-context-add-btn');
    const filesystemContextSelected = document.getElementById('filesystem-context-selected');
    const onedriveContextCount = document.getElementById('onedrive-context-count');
    const onedriveContextPath = document.getElementById('onedrive-context-path');
    const onedriveContextDescription = document.getElementById('onedrive-context-description');
    const onedriveContextBrowseBtn = document.getElementById('onedrive-context-browse-btn');
    const onedriveContextAddBtn = document.getElementById('onedrive-context-add-btn');
    const onedriveContextAddRootBtn = document.getElementById('onedrive-context-add-root-btn');
    const onedriveContextSelected = document.getElementById('onedrive-context-selected');
    const googleDriveContextCount = document.getElementById('google-drive-context-count');
    const googleDriveContextPath = document.getElementById('google-drive-context-path');
    const googleDriveContextDescription = document.getElementById('google-drive-context-description');
    const googleDriveContextBrowseBtn = document.getElementById('google-drive-context-browse-btn');
    const googleDriveContextAddBtn = document.getElementById('google-drive-context-add-btn');
    const googleDriveContextAddRootBtn = document.getElementById('google-drive-context-add-root-btn');
    const googleDriveContextSelected = document.getElementById('google-drive-context-selected');
    const contextTotalCount = document.getElementById('context-total-count');
    const contextSourcesSummary = document.getElementById('context-sources-summary');
    const googleToggle      = document.getElementById('google-toggle');
    const googleClientId    = document.getElementById('google-client-id');
    const googleClientSecret = document.getElementById('google-client-secret');
    const googleTestBtn     = document.getElementById('google-test-btn');
    const googleTestResult  = document.getElementById('google-test-result');
    const googleAuthBtn     = document.getElementById('google-auth-btn');
    const googleAuthStatus  = document.getElementById('google-auth-status');
    const microsoftToggle   = document.getElementById('microsoft-toggle');
    const microsoftClientId = document.getElementById('microsoft-client-id');
    const microsoftClientSecret = document.getElementById('microsoft-client-secret');
    const microsoftAuthBtn  = document.getElementById('microsoft-auth-btn');
    const microsoftTestBtn  = document.getElementById('microsoft-test-btn');
    const microsoftAuthStatus = document.getElementById('microsoft-auth-status');
    const microsoftTestResult = document.getElementById('microsoft-test-result');
    const slackToggle       = document.getElementById('slack-toggle');
    const slackBotToken     = document.getElementById('slack-bot-token');
    const slackAppToken     = document.getElementById('slack-app-token');
    const slackWebhookUrl   = document.getElementById('slack-webhook-url');
    const slackTestBtn      = document.getElementById('slack-test-btn');
    const slackTestResult   = document.getElementById('slack-test-result');
    const telegramToggle    = document.getElementById('telegram-toggle');
    const telegramBotToken  = document.getElementById('telegram-bot-token');
    const whatsappToggle    = document.getElementById('whatsapp-toggle');
    const browserHeadlessToggle = document.getElementById('browser-headless-toggle');
    const showThinkingToggle = document.getElementById('show-thinking-toggle');
    const stopOnCloseToggle = document.getElementById('stop-on-close-toggle');
    const browserSupportBadge = document.getElementById('browser-support-badge');
    const webhookList       = document.getElementById('webhook-list');
    const webhookAddBtn     = document.getElementById('webhook-add-btn');
    const webhookTemplate   = document.getElementById('webhook-item-template');
    const mcpServersInput   = document.getElementById('mcp-servers-input');
    const mcpServersNote    = document.getElementById('mcp-servers-note');
    const mcpServerList     = document.getElementById('mcp-server-list');
    const mcpServerCount    = document.getElementById('mcp-server-count');
    const mcpAddCustomBtn   = document.getElementById('mcp-add-custom-btn');
    const mcpPresetGallery  = document.getElementById('mcp-preset-gallery');
    const settingsModelWarning = document.getElementById('settings-model-warning');
    const policyBudgetModeSelect = document.getElementById('policy-budget-mode');
    const policyBudgetModeNote = document.getElementById('policy-budget-mode-note');
    const policyMaxStepsInput = document.getElementById('policy-max-steps');
    const policyMaxToolCallsInput = document.getElementById('policy-max-tool-calls');
    const policyMaxRetriesInput = document.getElementById('policy-max-retries');
    const policyMaxTokensInput = document.getElementById('policy-max-tokens');
    const policyMaxRuntimeInput = document.getElementById('policy-max-runtime');
    const policyBlockedToolsInput = document.getElementById('policy-blocked-tools');
    const policyApprovalToolsInput = document.getElementById('policy-approval-tools');
    const startBtn         = document.getElementById('start-btn');
    const chatBtn          = document.getElementById('chat-btn');
    const startBtnLabel    = document.getElementById('start-btn-label');
    const statusChip      = document.getElementById('status-chip');
    const statusText      = document.getElementById('status-text');
    const wsHealthEl      = document.getElementById('ws-health');
    const modelLabelEl    = document.getElementById('model-label');
    const appShell        = document.querySelector('.app');
    const paneResizer     = document.getElementById('pane-resizer');
    const agentPanel      = document.querySelector('.agent-panel');
    const explorationLayout = document.getElementById('exploration-layout');
    const explorationHistory = document.getElementById('exploration-history');

    const logContainer    = document.getElementById('log-container');
    const memoryContainer    = document.getElementById('memory-container');
    const sessionsContainer   = document.getElementById('sessions-container');
    const schedulesContainer  = document.getElementById('schedules-container');
    const identityContainer   = document.getElementById('identity-container');
    const memoryCount         = document.getElementById('memory-count');
    const clearSessionsBtn    = document.getElementById('clear-sessions-btn');
    const clearLogBtn         = document.getElementById('clear-log-btn');
    const internetToggle      = document.getElementById('internet-toggle');
    const emailToggle         = document.getElementById('email-toggle');
    const emailAccessHint     = document.getElementById('email-access-hint');
    const approvalToggle      = document.getElementById('approval-toggle');
    const speedToggle         = document.getElementById('speed-toggle');

    const COPILOT_QUICKSTARTS = [
        {
            id: 'morning-brief',
            label: 'Morning Brief',
            announce: 'Running Co-Pilot Morning Brief now.',
        },
        {
            id: 'follow-up',
            label: 'Follow-up',
            announce: 'Running Co-Pilot Follow-up now.',
        },
    ];
    const historyCollapseBtn  = document.getElementById('history-collapse-btn');
    const historyResizer      = document.getElementById('history-resizer');
    const chatThread          = document.getElementById('chat-thread');
    const chatThreadEmpty     = document.getElementById('chat-thread-empty');
    const chatBrowserLayout   = document.querySelector('.chat-browser-layout');
    const browserPreviewSection = document.getElementById('browser-preview-section');
    const browserPreviewStatus = document.getElementById('browser-preview-status');
    const browserPreviewImage = document.getElementById('browser-preview-image');
    const browserPreviewEmpty = document.getElementById('browser-preview-empty');
    const browserPreviewTitle = document.getElementById('browser-preview-title');
    const browserPreviewUrl = document.getElementById('browser-preview-url');
    const browserPreviewRefreshBtn = document.getElementById('browser-preview-refresh-btn');
    const browserPreviewCollapseBtn = document.getElementById('browser-preview-collapse-btn');
    const browserPreviewFullscreenBtn = document.getElementById('browser-preview-fullscreen-btn');
    const chatSessionPill     = document.getElementById('chat-session-pill');
    const workflowStatePill   = document.getElementById('workflow-state-pill');
    const workflowParallelHint = document.getElementById('workflow-parallel-hint');
    const pillStopWrapper  = document.getElementById('pill-stop-wrapper');
    const pillStopDropdown = document.getElementById('pill-stop-dropdown');
    const pillStopList     = document.getElementById('pill-stop-list');
    const pillStopAll      = document.getElementById('pill-stop-all');

    const themeToggleBtn = document.getElementById('theme-toggle-btn');
    const themeIconSun   = document.getElementById('theme-icon-sun');
    const themeIconMoon  = document.getElementById('theme-icon-moon');
    const HISTORY_COLLAPSED_STORAGE_KEY = 'agent_history_collapsed';
    const BROWSER_PREVIEW_COLLAPSED_STORAGE_KEY = 'agent_browser_preview_collapsed';

    const _browserPreviewModule = window.AgentFrontendModules.createBrowserPreviewModule({
        authFetch,
        authFetchJson,
        chatBrowserLayout,
        browserPreviewSection,
        browserPreviewStatus,
        browserPreviewImage,
        browserPreviewEmpty,
        browserPreviewTitle,
        browserPreviewUrl,
        browserPreviewRefreshBtn,
        browserPreviewCollapseBtn,
        browserPreviewFullscreenBtn,
        collapsedStorageKey: BROWSER_PREVIEW_COLLAPSED_STORAGE_KEY,
    });
    const setBrowserPreviewCollapsed = _browserPreviewModule.setBrowserPreviewCollapsed;
    const refreshBrowserPreview = _browserPreviewModule.refreshBrowserPreview;
    const _startBrowserPreviewPolling = _browserPreviewModule.startBrowserPreviewPolling;
    const _handleBrowserUpdate = _browserPreviewModule.handleBrowserUpdate;
    const _layoutShellModule = window.AgentFrontendModules.createLayoutShellModule({
        refs: {
            themeToggleBtn,
            themeIconSun,
            themeIconMoon,
            appShell,
            paneResizer,
            agentPanel,
            historyResizer,
            explorationLayout,
            explorationHistory,
        },
    });
    _layoutShellModule.init();

    let appSettings = {
        copilot_enabled: false,
        copilot_morning_brief_enabled: false,
        copilot_morning_brief_time: '08:30',
        copilot_followup_enabled: false,
        copilot_followup_time: '11:30',
        copilot_briefing_time: '08:30',
        copilot_autonomy_level: 'suggest',
        copilot_morning_brief_instructions: '',
        copilot_followup_instructions: '',
        copilot_instructions: '',
        outlook_enabled: false,
        google_enabled: false,
        google_client_id: '',
        google_client_secret: '',
        slack_enabled: false,
        slack_bot_token: '',
        slack_app_token: '',
        slack_webhook_url: '',
        telegram_enabled: false,
        telegram_bot_token: '',
        whatsapp_enabled: false,

        main_model: '',
        fast_model: '',
        fallback_model: '',
        nvidia_api_key: '',
        brave_api_key: '',
        ollama_cloud_api_key: '',
        openai_api_key: '',
        gemini_api_key: '',
        llamacpp_url: 'http://localhost:8080',
        llamacpp_model_path: '',
        llamacpp_extra_args: '',
        policy_budget_mode: 'budget',
        policy_max_steps_per_run: 8,
        policy_max_tool_calls_per_run: 20,
        policy_max_validation_retries_per_step: 1,
        policy_max_total_tokens_per_run: 120000,
        policy_max_runtime_seconds: 900,
        policy_blocked_tools: [],
        policy_require_approval_tools: ['run_shell', 'http_request', 'create_skill'],
        tool_enabled_map: {},
        webhooks: [],
        context_sources: [],
    };
    let runtimeCapabilities = {};
    let outlookAvailable = false;
    let _ftLastStores = [];
    let _gftLastStores = [];
    let _filesystemContextSources = {};
    let _onedriveContextSources = {};
    let _googleDriveContextSources = {};
    let _isLoadingSettings = false;
    let _contextAutoSaveTimer = null;

    /* ── Outlook folder tree state ────────────────────────── */
    let _ftSelectedFolders = {};   // { path: description }
    let _getOneDriveMode = () => 'local';
    let _openCloudBrowser = () => {};
    let _offlineOverlay = null;
    let _offlineSub = null;
    let _launcherCloseBtn = null;
    let _launcherStartBtn = null;
    let _launcherRestartBtn = null;
    let _launcherHint = null;
    let _launcherAvailable = false;
    let _offlineShown = false;
    let _offlineDismissed = false;
    let _launcherPollTimer = null;

    function escapeHtml(str) {
        return String(str || '')
            .replaceAll('&', '&amp;')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#39;');
    }

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

    const _contextSourcesModule = window.AgentFrontendModules.createContextSourcesModule({
        refs: {
            outlookSelectedDiv,
            googleSelectedDiv,
            filesystemContextSelected,
            onedriveContextSelected,
            googleDriveContextSelected,
            filesystemContextBrowseBtn,
            filesystemContextPath,
            filesystemContextRow,
            filesystemContextDescription,
            filesystemContextAddBtn,
            onedriveContextPath,
            onedriveContextDescription,
            onedriveContextAddBtn,
            onedriveContextBrowseBtn,
            onedriveContextAddRootBtn,
            googleDriveContextPath,
            googleDriveContextDescription,
            googleDriveContextAddBtn,
            googleDriveContextBrowseBtn,
            googleDriveContextAddRootBtn,
            outlookContextCount,
            googleContextCount,
            filesystemContextCount,
            onedriveContextCount,
            googleDriveContextCount,
            contextTotalCount,
            contextSourcesSummary,
        },
        helpers: {
            escapeHtml,
            authFetch,
            openDirBrowser: _mcpOpenDirBrowser,
            openCloudBrowser: (...args) => _openCloudBrowser(...args),
            getOutlookRenderTree: () => _ftRenderTree,
            getGoogleRenderTree: () => _gftRenderTree,
            ftCollect: () => _ftCollect(),
            gftCollect: () => _gftCollect(),
        },
        state: {
            getAppSettings: () => appSettings,
            getIsLoadingSettings: () => _isLoadingSettings,
            getContextAutoSaveTimer: () => _contextAutoSaveTimer,
            setContextAutoSaveTimer: (nextTimer) => { _contextAutoSaveTimer = nextTimer; },
            getOutlookSelectedFolders: () => _ftSelectedFolders,
            setOutlookSelectedFolders: (nextSelected) => {
                _ftSelectedFolders = nextSelected && typeof nextSelected === 'object' ? nextSelected : {};
            },
            getGoogleSelectedFolders: () => _gftSelectedFolders,
            setGoogleSelectedFolders: (nextSelected) => {
                _gftSelectedFolders = nextSelected && typeof nextSelected === 'object' ? nextSelected : {};
            },
            getFilesystemSources: () => _filesystemContextSources,
            setFilesystemSources: (nextSources) => {
                _filesystemContextSources = nextSources && typeof nextSources === 'object' ? nextSources : {};
            },
            getOneDriveSources: () => _onedriveContextSources,
            setOneDriveSources: (nextSources) => {
                _onedriveContextSources = nextSources && typeof nextSources === 'object' ? nextSources : {};
            },
            getGoogleDriveSources: () => _googleDriveContextSources,
            setGoogleDriveSources: (nextSources) => {
                _googleDriveContextSources = nextSources && typeof nextSources === 'object' ? nextSources : {};
            },
            getOutlookLastStores: () => _ftLastStores,
            getGoogleLastStores: () => _gftLastStores,
            getOneDriveMode: () => _getOneDriveMode(),
            getOneDriveLocalPathValue: () => onedriveLocalPath?.value.trim() || '',
        },
    });
    const _contextUiProvider = _contextSourcesModule.contextUiProvider;
    const _contextProviderLabel = _contextSourcesModule.contextProviderLabel;
    const _contextProviderMeta = _contextSourcesModule.contextProviderMeta;
    const _contextProviderSortRank = _contextSourcesModule.contextProviderSortRank;
    const _renderContextManagedSelections = _contextSourcesModule.renderContextManagedSelections;
    const _loadManagedContextSources = _contextSourcesModule.loadManagedContextSources;
    const _setEmailContextSectionState = _contextSourcesModule.setEmailContextSectionState;
    const _buildStructuralContextSources = _contextSourcesModule.buildStructuralContextSources;
    const _renderStructuralContextSummary = _contextSourcesModule.renderStructuralContextSummary;
    const _syncStructuralContextFromUi = _contextSourcesModule.syncStructuralContextFromUi;
    _contextSourcesModule.bindUi();

    const _composerAttachmentsModule = window.AgentFrontendModules.createComposerAttachmentsModule({
        authFetch,
        getBackendUrl: () => BACKEND_URL,
        getAppSettings: () => appSettings,
        refs: {
            attachBtn: document.getElementById('composer-attach-btn'),
            attachPopup: document.getElementById('composer-attach-popup'),
            composerInputRow: document.querySelector('.composer-input-row'),
            composerMoreSubmenu: document.getElementById('composer-more-submenu'),
            attachChips: document.getElementById('attachment-chips'),
            fileInput,
            fileLabel,
            goalInput,
            folderPathInput,
            folderClearBtn,
            contextInput,
            chatThread,
            onedriveModeLocal,
            onedriveModeGraph,
            onedriveLocalSettings,
            onedriveGraphSettings,
            onedriveDetectBtn,
            onedriveLocalPath,
            onedriveLocalStatus,
            attachGoogleDrive: document.getElementById('attach-google-drive'),
            attachOneDrive: document.getElementById('attach-onedrive'),
            cloudOverlay: document.getElementById('cloud-browser-overlay'),
            cloudList: document.getElementById('cloud-browser-list'),
            cloudTitle: document.getElementById('cloud-browser-title'),
            cloudPath: document.getElementById('cloud-browser-path'),
            cloudBack: document.getElementById('cloud-browser-back'),
            cloudClose: document.getElementById('cloud-browser-close'),
            cloudAttachBtn: document.getElementById('cloud-browser-attach'),
            cloudCountLabel: document.getElementById('cloud-browser-selected-count'),
        },
        helpers: {
            escapeHtml,
            appendChatBubble: (...args) => _appendChatBubble(...args),
            ensureChatThreadVisibleState: (...args) => _ensureChatThreadVisibleState(...args),
            compactChatThreadLayout: (...args) => _compactChatThreadLayout(...args),
            scrollChatToBottom: (...args) => _scrollChatToBottom(...args),
            contextUiProvider: _contextUiProvider,
            contextProviderLabel: _contextProviderLabel,
            contextProviderSortRank: _contextProviderSortRank,
            buildStructuralContextSources: _buildStructuralContextSources,
        },
        state: {
            getViewedHistorySessionId: () => _getViewedHistorySessionId(),
            getCurrentSessionId: () => currentSessionId,
            getPreferredConversationSessionId: () => _preferredConversationSessionId,
            getEmailReaderModal: () => _emailReaderModal,
            getFollowupEmailRow: () => _followupEmailRow,
            getFollowupCalendarRow: () => _followupCalendarRow,
            getRecalledMemoryRaw: () => recalledMemoryRaw,
            getInternetEnabled: () => !!internetToggle?.checked,
            getEmailEnabled: () => !!emailToggle?.checked,
        },
    });
    const _cloudAttachedFiles = _composerAttachmentsModule.cloudAttachedFiles;
    const _refreshAttachChips = _composerAttachmentsModule.refreshAttachChips;
    const _buildFolderAttachmentContext = _composerAttachmentsModule.buildFolderAttachmentContext;
    const _captureStructuralContextSnapshot = _composerAttachmentsModule.captureStructuralContextSnapshot;
    const _storeLastSentContextSnapshot = _composerAttachmentsModule.storeLastSentContextSnapshot;
    const _applyOneDriveMode = _composerAttachmentsModule.applyOneDriveMode;
    const _cloudUpdateStatus = _composerAttachmentsModule.cloudUpdateStatus;
    _openCloudBrowser = _composerAttachmentsModule.openCloudBrowser;
    _getOneDriveMode = _composerAttachmentsModule.getOneDriveMode;
    _composerAttachmentsModule.bindUi();

    let _gftSelectedFolders = {};   // { label-path: description }

    const _folderTreesModule = window.AgentFrontendModules.createFolderTreesModule({
        authFetchJson,
        onSelectionsChanged: () => _syncStructuralContextFromUi(),
        onRenderSelected: () => _renderContextManagedSelections(),
        outlook: {
            treeEl: outlookFolderTree,
            scanBtn: outlookScanBtn,
            getSelectedFolders: () => _ftSelectedFolders,
            setSelectedFolders: (nextSelected) => {
                _ftSelectedFolders = nextSelected && typeof nextSelected === 'object' ? nextSelected : {};
            },
            setLastStores: (nextStores) => {
                _ftLastStores = Array.isArray(nextStores) ? nextStores : [];
            },
        },
        google: {
            treeEl: googleFolderTree,
            scanBtn: googleScanBtn,
            getSelectedFolders: () => _gftSelectedFolders,
            setSelectedFolders: (nextSelected) => {
                _gftSelectedFolders = nextSelected && typeof nextSelected === 'object' ? nextSelected : {};
            },
            setLastStores: (nextStores) => {
                _gftLastStores = Array.isArray(nextStores) ? nextStores : [];
            },
        },
    });
    const _ftRenderTree = _folderTreesModule.ftRenderTree;
    const _ftLoadFromSettings = _folderTreesModule.ftLoadFromSettings;
    const _ftCollect = _folderTreesModule.ftCollect;
    const _gftRenderTree = _folderTreesModule.gftRenderTree;
    const _gftLoadFromSettings = _folderTreesModule.gftLoadFromSettings;
    const _gftCollect = _folderTreesModule.gftCollect;
    _folderTreesModule.bindScanHandlers();
    /* ── end folder trees ─────────────────────────────────── */

    let availableModels = [];
    let _modelsLoading = false;
    const RECOMMENDED_OLLAMA_PROFILE = {
        main: 'glm-5.1:cloud',
        fast: 'gpt-oss:20b-cloud',
        fallback: 'qwen3.5:9b',
    };
    const COPILOT_SCHEDULE_CONFIGS = {
        'morning-brief': {
            id: 'morning-brief',
            enabledKey: 'copilot_morning_brief_enabled',
            timeKey: 'copilot_morning_brief_time',
            source: 'copilot_morning_brief',
            localId: 'copilot_morning_brief_local',
            label: 'Co-Pilot Morning Brief',
            statusTarget: 'morning',
        },
        'follow-up': {
            id: 'follow-up',
            enabledKey: 'copilot_followup_enabled',
            timeKey: 'copilot_followup_time',
            source: 'copilot_followup',
            localId: 'copilot_followup_local',
            label: 'Co-Pilot Follow-up',
            statusTarget: 'followup',
        },
    };
    let _appRuntimeModule = null;

    function _runCopilotById(...args) { return _appRuntimeModule ? _appRuntimeModule.runCopilotById(...args) : undefined; }
    function _runSelectedCopilotQuickstart(...args) { return _appRuntimeModule ? _appRuntimeModule.runSelectedCopilotQuickstart(...args) : undefined; }
    function _loadSchedules(...args) { return _appRuntimeModule ? _appRuntimeModule.loadSchedules(...args) : []; }
    function _formatInterval(...args) { return _appRuntimeModule ? _appRuntimeModule.formatInterval(...args) : ''; }
    function _saveSchedules(...args) { return _appRuntimeModule ? _appRuntimeModule.saveSchedules(...args) : undefined; }
    function _createRunLane(...args) { return _appSessionModule ? _appSessionModule.createRunLane(...args) : undefined; }
    function addSchedule(...args) { return _appRuntimeModule ? _appRuntimeModule.addSchedule(...args) : undefined; }
    function removeSchedule(...args) { return _appRuntimeModule ? _appRuntimeModule.removeSchedule(...args) : undefined; }
    function toggleSchedule(...args) { return _appRuntimeModule ? _appRuntimeModule.toggleSchedule(...args) : undefined; }
    function _executeScheduledRun(...args) { return _appRuntimeModule ? _appRuntimeModule.executeScheduledRun(...args) : undefined; }
    function _schedulePollingTick(...args) { return _appRuntimeModule ? _appRuntimeModule.schedulePollingTick(...args) : undefined; }
    function _updateScheduleCountdowns(...args) { return _appRuntimeModule ? _appRuntimeModule.updateScheduleCountdowns(...args) : undefined; }
    function _startSchedulePolling(...args) { return _appRuntimeModule ? _appRuntimeModule.startSchedulePolling(...args) : undefined; }
    function _stopSchedulePolling(...args) { return _appRuntimeModule ? _appRuntimeModule.stopSchedulePolling(...args) : undefined; }
    function _initScheduleTimers(...args) { return _appRuntimeModule ? _appRuntimeModule.initScheduleTimers(...args) : undefined; }
    function renderSchedules(...args) { return _appRuntimeModule ? _appRuntimeModule.renderSchedules(...args) : undefined; }
    function _showSchedulePopover(...args) { return _appRuntimeModule ? _appRuntimeModule.showSchedulePopover(...args) : undefined; }
    function _updateSchedulePickerUI(...args) { return _appRuntimeModule ? _appRuntimeModule.updateSchedulePickerUI(...args) : undefined; }
    function _toggleScheduleDropdown(...args) { return _appRuntimeModule ? _appRuntimeModule.toggleScheduleDropdown(...args) : undefined; }
    function _resetDraftReplyState(...args) { return _appRuntimeModule ? _appRuntimeModule.resetDraftReplyState(...args) : undefined; }
    function _sendDraftReplyRequest(...args) { return _appRuntimeModule ? _appRuntimeModule.sendDraftReplyRequest(...args) : undefined; }
    function _retryDraftReplyNow(...args) { return _appRuntimeModule ? _appRuntimeModule.retryDraftReplyNow(...args) : undefined; }
    function _showDraftReplyRetryState(...args) { return _appRuntimeModule ? _appRuntimeModule.showDraftReplyRetryState(...args) : undefined; }
    function _consumePendingInlineEmailReply(...args) { return _appRuntimeModule ? _appRuntimeModule.consumePendingInlineEmailReply(...args) : false; }
    function _resolveEmailInlineReply(...args) { return _appRuntimeModule ? _appRuntimeModule.resolveEmailInlineReply(...args) : false; }
    function _openEmailInlineReader(...args) { return _appRuntimeModule ? _appRuntimeModule.openEmailInlineReader(...args) : undefined; }
    function _closeEmailInlineReader(...args) { return _appRuntimeModule ? _appRuntimeModule.closeEmailInlineReader(...args) : undefined; }
    function _openCalendarInlineReader(...args) { return _appRuntimeModule ? _appRuntimeModule.openCalendarInlineReader(...args) : undefined; }
    function _closeCalendarInlineReader(...args) { return _appRuntimeModule ? _appRuntimeModule.closeCalendarInlineReader(...args) : undefined; }
    function executeComposerAction(...args) { return _appRuntimeModule ? _appRuntimeModule.executeComposerAction(...args) : undefined; }
    function sendStart(...args) { return _appRuntimeModule ? _appRuntimeModule.sendStart(...args) : undefined; }

    function _initAppRuntimeModule() {
        if (_appRuntimeModule) return;
        const _appRuntimeScope = {};
        Object.defineProperties(_appRuntimeScope, {
            BACKEND_URL: { get: () => BACKEND_URL, set: (value) => { BACKEND_URL = value; }, enumerable: true },
            appSettings: { get: () => appSettings, set: (value) => { appSettings = value; }, enumerable: true },
            availableModels: { get: () => availableModels, set: (value) => { availableModels = Array.isArray(value) ? value : []; }, enumerable: true },
            _modelsLoading: { get: () => _modelsLoading, set: (value) => { _modelsLoading = !!value; }, enumerable: true },
            currentSessionId: { get: () => currentSessionId, set: (value) => { currentSessionId = value; }, enumerable: true },
            _preferredConversationSessionId: { get: () => _preferredConversationSessionId, set: (value) => { _preferredConversationSessionId = value; }, enumerable: true },
            _freshChatRequested: { get: () => _freshChatRequested, set: (value) => { _freshChatRequested = !!value; }, enumerable: true },
            _pendingSessionBootstrap: { get: () => _pendingSessionBootstrap, set: (value) => { _pendingSessionBootstrap = !!value; }, enumerable: true },
            _queuedBootstrapFollowUps: { get: () => _queuedBootstrapFollowUps, set: (value) => { _queuedBootstrapFollowUps = Array.isArray(value) ? value : []; }, enumerable: true },
            _pendingPreRunChatBubbles: { get: () => _pendingPreRunChatBubbles, set: (value) => { _pendingPreRunChatBubbles = Array.isArray(value) ? value : []; }, enumerable: true },
            _currentHandlingRunId: { get: () => _currentHandlingRunId, set: (value) => { _currentHandlingRunId = value; }, enumerable: true },
            activeRunId: { get: () => activeRunId, set: (value) => { activeRunId = value; }, enumerable: true },
            ws: { get: () => ws, set: (value) => { ws = value; }, enumerable: true },
            _reconnectTimer: { get: () => _reconnectTimer, set: (value) => { _reconnectTimer = value; }, enumerable: true },
            recalledMemoryRaw: { get: () => recalledMemoryRaw, set: (value) => { recalledMemoryRaw = value; }, enumerable: true },
            _cloudAttachedFiles: { get: () => _cloudAttachedFiles, set: (value) => { _cloudAttachedFiles = Array.isArray(value) ? value : []; }, enumerable: true },
            _pendingEmailReaderRow: { get: () => _pendingEmailReaderRow, set: (value) => { _pendingEmailReaderRow = value; }, enumerable: true },
            _emailReaderModal: { get: () => _emailReaderModal, set: (value) => { _emailReaderModal = value; }, enumerable: true },
            _followupEmailRow: { get: () => _followupEmailRow, set: (value) => { _followupEmailRow = value; }, enumerable: true },
            _draftReplyPending: { get: () => _draftReplyPending, set: (value) => { _draftReplyPending = !!value; }, enumerable: true },
            _draftReplyRetryCount: { get: () => _draftReplyRetryCount, set: (value) => { _draftReplyRetryCount = Number.isFinite(Number(value)) ? Number(value) : 0; }, enumerable: true },
            _draftReplyRequest: { get: () => _draftReplyRequest, set: (value) => { _draftReplyRequest = value; }, enumerable: true },
        });
        Object.assign(_appRuntimeScope, {
            runs,
            COPILOT_QUICKSTARTS,
            schedulesContainer,
            schedulePickerBtn,
            schedulePickerLabel,
            schedulePickerDropdown,
            scheduleIntervalRow,
            scheduleFirstRunRow,
            scheduleFirstRunInput,
            scheduleIntervalInput,
            scheduleUnit,
            schedCheckOnce,
            schedCheckRepeat,
            startBtn,
            startBtnLabel,
            goalInput,
            contextInput,
            chatThread,
            copilotQuickstartSelect,
            copilotRunNowBtn,
            fileInput,
            fileLabel,
            folderPathInput,
            folderClearBtn,
            memoryContainer,
            memoryCount,
            tokenCount,
            budgetCount,
            tokenCounterEl,
            budgetCounterEl,
            internetToggle,
            approvalToggle,
            speedToggle,
            emailToggle,
            authFetch,
            authFetchJson,
            setStatus,
            showToast,
            escapeHtml,
            _renderChatMarkdown,
            _appendChatBubble,
            _clearChatThread,
            _scrollChatToBottom,
            _showThinkingBubble,
            _forceStickChatBottom,
            _collectCopilotStateFromUi,
            _setAllCopilotStatuses,
            _setCopilotStatus,
            _buildCopilotFollowupGoal,
            _buildCopilotMorningBriefGoal,
            _updateCopilotStatusFromSchedules,
            _refreshComposerModeUi,
            _findRunIdBySessionId,
            _ensureChatLane,
            _setActiveRun,
            _activeRunCount,
            _activeLlmRunCount,
            getViewedHistorySessionId: () => _getViewedHistorySessionId(),
            appendLog,
            _showThinkingBubble,
            _removeThinkingBubble,
            connectWebSocket,
            _resolveSessionForRun,
            _composerPreferredSessionId,
            _fetchSession,
            _renderSessionChatHistory,
            _buildFolderAttachmentContext,
            _refreshAttachChips,
            _storeLastSentContextSnapshot,
            _captureStructuralContextSnapshot,
            _parseComposerInput,
            _setChatSessionPill,
            _setViewedHistorySession,
        });
        _appRuntimeModule = window.AgentFrontendModules.createAppRuntimeModule(_appRuntimeScope);
    }

    const _copilotModule = window.AgentFrontendModules.createCopilotModule({
        getSettings: () => appSettings,
        setSettings: (nextSettings) => { appSettings = nextSettings; },
        settingsOverlay,
        copilotEnabledSettingsToggle,
        copilotMorningBriefToggle,
        copilotBriefingTimeInput,
        copilotFollowupToggle,
        copilotFollowupTimeInput,
        copilotAutonomyLevelInput,
        copilotMorningInstructionsInput,
        copilotFollowupInstructionsInput,
        copilotQuickstartSelect,
        copilotQuickstartBtn,
        copilotRunNowBtn,
        copilotFollowupRunBtn,
        copilotMorningStatusNote,
        copilotFollowupStatusNote,
        copilotQuickstarts: COPILOT_QUICKSTARTS,
        copilotScheduleConfigs: COPILOT_SCHEDULE_CONFIGS,
        loadSchedules: _loadSchedules,
        saveSchedules: _saveSchedules,
        startSchedulePolling: _startSchedulePolling,
        renderSchedules,
        authFetchJson,
        backendUrl: BACKEND_URL,
    });

    const _normalizeCopilotTime = _copilotModule.normalizeCopilotTime;
    const _normalizeFollowupTime = _copilotModule.normalizeFollowupTime;
    const _normalizeCopilotSettings = _copilotModule.normalizeCopilotSettings;
    const _collectCopilotStateFromUi = _copilotModule.collectCopilotStateFromUi;
    const _copilotAutonomySummary = _copilotModule.copilotAutonomySummary;
    const _setCopilotStatus = _copilotModule.setCopilotStatus;
    const _setAllCopilotStatuses = _copilotModule.setAllCopilotStatuses;
    const _syncCopilotEnabledToggles = _copilotModule.syncCopilotEnabledToggles;
    const _computeCopilotFirstRunIso = _copilotModule.computeCopilotFirstRunIso;
    const _buildCopilotMorningBriefGoal = _copilotModule.buildCopilotMorningBriefGoal;
    const _buildCopilotFollowupGoal = _copilotModule.buildCopilotFollowupGoal;
    const _copilotScheduleDefinitions = _copilotModule.copilotScheduleDefinitions;
    const _upsertLocalCopilotSchedules = _copilotModule.upsertLocalCopilotSchedules;
    const _syncCopilotSchedules = _copilotModule.syncCopilotSchedules;
    const _saveCopilotSettings = _copilotModule.saveCopilotSettings;
    const _populateCopilotQuickstartOptions = _copilotModule.populateCopilotQuickstartOptions;
    const _refreshCopilotPanelUi = _copilotModule.refreshCopilotPanelUi;
    const _applyCopilotUiFromSettings = _copilotModule.applyCopilotUiFromSettings;
    const _findCopilotSchedule = _copilotModule.findCopilotSchedule;
    const _updateCopilotStatusFromSchedules = _copilotModule.updateCopilotStatusFromSchedules;

    const _settingsIntegrationsModule = window.AgentFrontendModules.createSettingsIntegrationsModule({
        authFetchJson,
        outlookTestBtn,
        outlookTestResult,
        googleAuthBtn,
        googleAuthStatus,
        googleTestBtn,
        googleTestResult,
        microsoftAuthBtn,
        microsoftAuthStatus,
        microsoftTestBtn,
        microsoftTestResult,
        slackTestBtn,
        slackTestResult,
    });
    const _identityModule = window.AgentFrontendModules.createIdentityModule({
        authFetchJson,
        backendUrl: BACKEND_URL,
        identityContainer,
        memoryCount,
        escapeHtml,
    });
    const loadIdentity = _identityModule.loadIdentity;

    const _waStatusDot = document.getElementById('whatsapp-status-dot');
    const _waStatusText = document.getElementById('whatsapp-status-text');
    const _waQrContainer = document.getElementById('whatsapp-qr-container');
    const _waQrImg = document.getElementById('whatsapp-qr-img');
    const _waConnInfo = document.getElementById('whatsapp-connected-info');
    const _waPhoneDisp = document.getElementById('whatsapp-phone-display');
    const _waConnSince = document.getElementById('whatsapp-connected-since');
    const _waErrorBox = document.getElementById('whatsapp-error');
    const _waStartBtn = document.getElementById('whatsapp-start-btn');
    const _waRelinkBtn = document.getElementById('whatsapp-relink-btn');
    const _waDisconnBtn = document.getElementById('whatsapp-disconnect-btn');
    const _waTestPanel = document.getElementById('whatsapp-test-panel');
    const _waTestSendBtn = document.getElementById('whatsapp-test-send-btn');
    const _waTestResult = document.getElementById('whatsapp-test-result');
    const _waSetupProgress = document.getElementById('whatsapp-setup-progress');
    const _waSetupStep = document.getElementById('whatsapp-setup-step');
    const _whatsAppModule = window.AgentFrontendModules.createWhatsAppModule({
        authFetch,
        authFetchJson,
        refs: {
            statusDot: _waStatusDot,
            statusText: _waStatusText,
            qrContainer: _waQrContainer,
            qrImage: _waQrImg,
            connectedInfo: _waConnInfo,
            phoneDisplay: _waPhoneDisp,
            connectedSince: _waConnSince,
            errorBox: _waErrorBox,
            startButton: _waStartBtn,
            relinkButton: _waRelinkBtn,
            disconnectButton: _waDisconnBtn,
            testPanel: _waTestPanel,
            testSendButton: _waTestSendBtn,
            testResult: _waTestResult,
            setupProgress: _waSetupProgress,
            setupStep: _waSetupStep,
        },
    });
    const _waRefreshStatus = _whatsAppModule.refreshStatus;
    const _disposeWhatsApp = _whatsAppModule.dispose;
    _whatsAppModule.bindUi();

    function _asPositiveInt(value, fallback, min = 1) {
        const n = parseInt(value, 10);
        if (!Number.isFinite(n)) return fallback;
        return Math.max(min, n);
    }

    function _skillNameForDisplay(name) {
        return String(name || '').trim() === 'create_tool' ? 'create_skill' : String(name || '').trim();
    }

    function _toolsToCsv(value) {
        return Array.isArray(value) ? value.map(_skillNameForDisplay).join(', ') : '';
    }

    function _csvToTools(value) {
        return String(value || '')
            .split(',')
            .map(t => t.trim())
            .map(_skillNameForDisplay)
            .filter(Boolean);
    }

    async function _parseWebSocketMessage(event) {
        const raw = event?.data;
        if (typeof raw === 'string') {
            return JSON.parse(raw);
        }
        if (raw instanceof Blob) {
            return JSON.parse(await raw.text());
        }
        if (raw instanceof ArrayBuffer) {
            return JSON.parse(new TextDecoder().decode(raw));
        }
        if (ArrayBuffer.isView(raw)) {
            return JSON.parse(new TextDecoder().decode(raw));
        }
        return JSON.parse(String(raw || ''));
    }

    const ollamaLibrarySelect  = document.getElementById('ollama-library-select');
    const ollamaPullBtn        = document.getElementById('ollama-pull-btn');
    const ollamaPullStatus     = document.getElementById('ollama-pull-status');
    const ollamaLibraryRefresh = document.getElementById('ollama-library-refresh');
    const ollamaCustomModel    = document.getElementById('ollama-custom-model');
    const ollamaCustomPullBtn  = document.getElementById('ollama-custom-pull-btn');
    const ollamaInstalledList  = document.getElementById('ollama-installed-list');
    const _modelSettingsModule = window.AgentFrontendModules.createModelSettingsModule({
        authFetch,
        authFetchJson,
        getBackendUrl: () => BACKEND_URL,
        recommendedOllamaProfile: RECOMMENDED_OLLAMA_PROFILE,
        getAppSettings: () => appSettings,
        getRuntimeCapabilities: () => runtimeCapabilities,
        getAvailableModels: () => availableModels,
        setAvailableModels: (nextModels) => { availableModels = Array.isArray(nextModels) ? nextModels : []; },
        getModelsLoading: () => _modelsLoading,
        setModelsLoading: (nextLoading) => { _modelsLoading = !!nextLoading; },
        onModelUiStateChange: () => { _refreshComposerModeUi(); },
        refs: {
            modelSelect,
            fastModelSelect,
            fallbackModelSelect,
            modelLabelEl,
            composerSendBtn,
            modelWarning,
            settingsModelWarning,
            ollamaCloudNote,
            ollamaApiNote,
            nvidiaNote,
            braveNote,
            openaiNote,
            geminiNote,
            browserSupportBadge,
            ollamaApplyRecommendedBtn,
            ollamaPullRecommendedBtn,
            ollamaLibrarySelect,
            ollamaPullBtn,
            ollamaPullStatus,
            ollamaLibraryRefresh,
            ollamaCustomModel,
            ollamaCustomPullBtn,
            ollamaInstalledList,
        },
    });
    const refreshProviderNotes = _modelSettingsModule.refreshProviderNotes;
    const _updateModelLabel = _modelSettingsModule.updateModelLabel;
    const applyModelSettingsToUi = _modelSettingsModule.applyModelSettingsToUi;
    const loadModels = _modelSettingsModule.loadModels;
    const loadOllamaLibrary = _modelSettingsModule.loadOllamaLibrary;
    const pullOllamaModel = _modelSettingsModule.pullOllamaModel;
    const pullRecommendedOllamaProfile = _modelSettingsModule.pullRecommendedOllamaProfile;
    const loadOllamaInstalled = _modelSettingsModule.loadOllamaInstalled;
    const deleteOllamaModel = _modelSettingsModule.deleteOllamaModel;
    _modelSettingsModule.bindOllamaUi();
    const _settingsPanelModule = window.AgentFrontendModules.createSettingsPanelModule({
        authFetch,
        authFetchJson,
        getAppSettings: () => appSettings,
        setAppSettings: (nextSettings) => { appSettings = nextSettings; },
        getOutlookAvailable: () => outlookAvailable,
        setOutlookAvailable: (nextAvailable) => { outlookAvailable = !!nextAvailable; },
        setRuntimeCapabilities: (nextCapabilities) => { runtimeCapabilities = nextCapabilities || {}; },
        setIsLoadingSettings: (nextValue) => { _isLoadingSettings = !!nextValue; },
        clearPendingContextSave: () => {
            if (_contextAutoSaveTimer) {
                clearTimeout(_contextAutoSaveTimer);
                _contextAutoSaveTimer = null;
            }
        },
        refs: {
            settingsOverlay,
            nvidiaApiKeyInput,
            braveApiKeyInput,
            ollamaCloudApiKeyInput,
            openaiApiKeyInput,
            geminiApiKeyInput,
            ollamaCloudApiKeyTestBtn,
            openaiApiKeyTestBtn,
            geminiApiKeyTestBtn,
            nvidiaApiKeyTestBtn,
            ollamaCloudApiKeyTestStatus,
            openaiApiKeyTestStatus,
            geminiApiKeyTestStatus,
            nvidiaApiKeyTestStatus,
            llamacppUrlInput,
            llamacppModelPathInput,
            llamacppExtraArgsInput,
            outlookToggle,
            googleToggle,
            googleClientId,
            googleClientSecret,
            googleAuthStatus,
            googleFolderSection,
            googleScanBtn,
            googleContextStatus,
            microsoftToggle,
            microsoftClientId,
            microsoftClientSecret,
            microsoftAuthStatus,
            onedriveModeLocal,
            onedriveModeGraph,
            onedriveLocalPath,
            onedriveContextHint,
            googleDriveContextHint,
            onedriveLocalStatus,
            slackToggle,
            slackBotToken,
            slackAppToken,
            slackWebhookUrl,
            telegramToggle,
            telegramBotToken,
            whatsappToggle,
            policyBudgetModeSelect,
            policyMaxStepsInput,
            policyMaxToolCallsInput,
            policyMaxRetriesInput,
            policyMaxTokensInput,
            policyMaxRuntimeInput,
            policyBlockedToolsInput,
            policyApprovalToolsInput,
            browserHeadlessToggle,
            showThinkingToggle,
            mcpServersInput,
            mcpServersNote,
            outlookFolderSection,
            outlookScanBtn,
            outlookContextStatus,
            outlookBadge,
            outlookNote,
            emailToggle,
            emailAccessHint,
            modelSelect,
            fastModelSelect,
            fallbackModelSelect,
        },
        helpers: {
            asPositiveInt: _asPositiveInt,
            csvToTools: _csvToTools,
            toolsToCsv: _toolsToCsv,
            normalizeCopilotSettings: _normalizeCopilotSettings,
            applyCopilotUiFromSettings: _applyCopilotUiFromSettings,
            loadManagedContextSources: _loadManagedContextSources,
            ftLoadFromSettings: _ftLoadFromSettings,
            gftLoadFromSettings: _gftLoadFromSettings,
            syncStructuralContextFromUi: _syncStructuralContextFromUi,
            setEmailContextSectionState: _setEmailContextSectionState,
            cloudUpdateStatus: _cloudUpdateStatus,
            applyOneDriveMode: _applyOneDriveMode,
            waRefreshStatus: _waRefreshStatus,
            normalizePolicyBudgetMode: _normalizePolicyBudgetMode,
            updatePolicyBudgetModeUi: _updatePolicyBudgetModeUi,
            mcpRenderServerList: _mcpRenderServerList,
            mcpUpdatePresetChips: _mcpUpdatePresetChips,
            mcpFetchStatus: _mcpFetchStatus,
            whRenderList: _whRenderList,
            whCollect: _whCollect,
            applyModelSettingsToUi,
            refreshProviderNotes,
            collectCopilotStateFromUi: _collectCopilotStateFromUi,
            ftCollect: _ftCollect,
            gftCollect: _gftCollect,
            syncCopilotSchedules: _syncCopilotSchedules,
            loadSchedules: _loadSchedules,
            updateCopilotStatusFromSchedules: _updateCopilotStatusFromSchedules,
            renderSchedules,
            loadModels,
            setAllCopilotStatuses: _setAllCopilotStatuses,
            showToast,
        },
    });
    const loadSettings = _settingsPanelModule.loadSettings;
    const _resetSettingsPanel = _settingsPanelModule.resetUiFromSettings;
    const _saveSettingsPanel = _settingsPanelModule.saveSettings;
    _settingsPanelModule.bindApiKeyTestButtons();
    const _settingsKnowledgeModule = window.AgentFrontendModules.createSettingsKnowledgeModule({
        authFetch,
        authFetchJson,
        helpers: {
            escapeHtml,
            showToast,
            loadOllamaLibrary,
            loadOllamaInstalled,
        },
        state: {
            getAppSettings: () => appSettings,
        },
    });
    const _loadWikiStatus = _settingsKnowledgeModule.loadWikiStatus;
    _settingsKnowledgeModule.bindUi();
    const _auxUiModule = window.AgentFrontendModules.createAuxUiModule({
        authFetch,
        authFetchJson,
        getBackendUrl: () => BACKEND_URL,
        refs: {
            settingsBtn,
            settingsOverlay,
            settingsCloseBtn,
            settingsCancelBtn,
            helpBtn: document.getElementById('help-btn'),
            helpOverlay: document.getElementById('help-overlay'),
            helpCloseBtn: document.getElementById('help-close-btn'),
            llamacppDetectBtn,
            llamacppRunStatus,
            llamacppOllamaModelsSel,
            llamacppModelPathInput,
            llamacppStopBtn,
            llamacppInstallBtn,
        },
        helpers: {
            loadWikiStatus: _loadWikiStatus,
            resetSettingsPanel: _resetSettingsPanel,
            refreshProviderNotes,
        },
    });
    const _refreshLlamacppRunStatus = _auxUiModule.refreshLlamacppRunStatus;
    _auxUiModule.bindUi();

    // Disclaimer
    const disclaimerOverlay  = document.getElementById('disclaimer-overlay');
    const disclaimerAcceptBtn = document.getElementById('disclaimer-accept-btn');
    const disclaimerDeclineBtn = document.getElementById('disclaimer-decline-btn');
    const startServerBtn      = document.getElementById('start-server-btn');
    const resetServerBtn      = document.getElementById('reset-server-btn');
    const resetServerNote     = document.getElementById('reset-server-note');
    const stopServerBtn = document.getElementById('stop-server-btn');
    const updateCurrentVersionInput = document.getElementById('update-current-version');
    const updateLatestVersionInput = document.getElementById('update-latest-version');
    const updateCheckBtn = document.getElementById('update-check-btn');
    const updateInstallBtn = document.getElementById('update-install-btn');
    const updateNote = document.getElementById('update-note');
    const updateDetails = document.getElementById('update-details');
    const systemCheckBtn = document.getElementById('system-check-btn');
    const systemCheckResults = document.getElementById('system-check-results');
    const appVersionEls = document.querySelectorAll('[data-app-version]');

    // ── Setup Wizard ────────────────────────────────────────────────────────
    const setupOverlay   = document.getElementById('setup-overlay');
    const setupSteps     = document.querySelectorAll('#setup-steps .setup-step');
    const setupDots      = document.querySelectorAll('.setup-dot');
    const setupProgressBar = document.getElementById('setup-progress-bar');
    const setupBackBtn   = document.getElementById('setup-back-btn');
    const setupNextBtn   = document.getElementById('setup-next-btn');

    // Folder picker refs
    const setupInstallDir  = document.getElementById('setup-install-dir');
    const setupCustomDir   = document.getElementById('setup-custom-dir');
    const setupValidateBtn = document.getElementById('setup-validate-dir-btn');
    const setupDirStatus   = document.getElementById('setup-dir-status');

    // Wizard toggle refs
    const setupOpenaiToggle  = document.getElementById('setup-openai-toggle');
    const setupGeminiToggle  = document.getElementById('setup-gemini-toggle');
    const setupNvidiaToggle  = document.getElementById('setup-nvidia-toggle');
    const setupBraveToggle   = document.getElementById('setup-brave-toggle');
    const setupGoogleToggle  = document.getElementById('setup-google-toggle');
    const setupSlackToggle   = document.getElementById('setup-slack-toggle');
    const setupWhatsappToggle = document.getElementById('setup-whatsapp-toggle');
    const setupTelegramToggle = document.getElementById('setup-telegram-toggle');
    const setupOutlookToggle = document.getElementById('setup-outlook-toggle');

    // Wizard input refs
    const setupOpenaiKey     = document.getElementById('setup-openai-key');
    const setupGeminiKey     = document.getElementById('setup-gemini-key');
    const setupNvidiaKey     = document.getElementById('setup-nvidia-key');
    const setupBraveKey      = document.getElementById('setup-brave-key');
    const setupGoogleClientId = document.getElementById('setup-google-client-id');
    const setupGoogleClientSecret = document.getElementById('setup-google-client-secret');
    const setupSlackBot      = document.getElementById('setup-slack-bot');
    const setupSlackApp      = document.getElementById('setup-slack-app');
    const setupTelegramToken = document.getElementById('setup-telegram-token');
    const setupCancelBtn = document.getElementById('setup-cancel-btn');
    const _setupWizardModule = window.AgentFrontendModules.createSetupWizardModule({
        authFetch,
        authFetchJson,
        getAppSettings: () => appSettings,
        recommendedOllamaProfile: RECOMMENDED_OLLAMA_PROFILE,
        loadSettings,
        loadModels,
        hideOfflineOverlay: _hideOfflineOverlay,
        setIsLoadingSettings: (nextValue) => { _isLoadingSettings = !!nextValue; },
        refs: {
            disclaimerOverlay,
            setupOverlay,
            setupSteps,
            setupDots,
            setupProgressBar,
            setupBackBtn,
            setupNextBtn,
            setupInstallDir,
            setupCustomDir,
            setupValidateBtn,
            setupDirStatus,
            setupOpenaiToggle,
            setupGeminiToggle,
            setupNvidiaToggle,
            setupBraveToggle,
            setupGoogleToggle,
            setupSlackToggle,
            setupWhatsappToggle,
            setupTelegramToggle,
            setupOutlookToggle,
            setupOpenaiKey,
            setupGeminiKey,
            setupNvidiaKey,
            setupBraveKey,
            setupGoogleClientId,
            setupGoogleClientSecret,
            setupSlackBot,
            setupSlackApp,
            setupTelegramToken,
            setupCancelBtn,
        },
    });
    const _maybeShowSetupWizard = _setupWizardModule.maybeShowSetupWizard;
    const _bootstrapStartupGates = _setupWizardModule.bootstrapStartupGates;
    _setupWizardModule.bindUi();

    const _systemPanelModule = window.AgentFrontendModules.createSystemPanelModule({
        authFetch,
        authFetchJson,
        launcherUrl: LAUNCHER_URL,
        refs: {
            startServerBtn,
            resetServerBtn,
            resetServerNote,
            stopServerBtn,
            systemCheckBtn,
            systemCheckResults,
            updateCurrentVersionInput,
            updateLatestVersionInput,
            updateCheckBtn,
            updateInstallBtn,
            updateNote,
            updateDetails,
            appVersionEls,
        },
        helpers: {
            waitForBackendReady: _waitForBackendReady,
            pollLauncherStatus: _pollLauncherStatus,
            resolveApiUrl: _resolveApiUrl,
            connectWebSocket,
            escapeHtml,
            loadSettings,
        },
    });
    const loadVersionInfo = _systemPanelModule.loadVersionInfo;
    const checkForUpdate = _systemPanelModule.checkForUpdate;
    _systemPanelModule.bindUi();

    const _systemOverlayIds = ['disclaimer-overlay', 'setup-overlay', 'server-offline-overlay', 'cloud-browser-overlay'];
    const _userOverlayIds = ['settings-overlay', 'help-overlay'];
    const _softBlockingOverlayIds = [..._systemOverlayIds, ..._userOverlayIds];

    function _isElementVisible(el) {
        if (!el) return false;
        const style = window.getComputedStyle(el);
        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
        return el.getClientRects().length > 0;
    }

    function _hideSoftBlockingOverlays(reason = 'auto-unfreeze') {
        // On ws-open, skip user-intentional overlays (settings, help) to avoid
        // auto-dismissing panels the user opened deliberately.
        const ids = reason === 'ws-open' ? _systemOverlayIds : _softBlockingOverlayIds;
        ids.forEach((id) => {
            const el = document.getElementById(id);
            if (!_isElementVisible(el)) return;
            el.style.display = 'none';
            el.setAttribute('aria-hidden', 'true');
            console.warn(`[ui] hid blocking overlay: ${id} (${reason})`);
        });
        document.body.classList.remove('pane-resizing', 'history-resizing');
    }

    function _collectOverlayProbePoints() {
        const points = [];
        const centerX = Math.max(10, Math.floor(window.innerWidth / 2));
        const centerY = Math.max(10, Math.floor(window.innerHeight / 2));
        points.push([centerX, centerY]);
        if (goalInput) {
            const rect = goalInput.getBoundingClientRect();
            if (rect.width > 0 && rect.height > 0) {
                points.push([
                    Math.floor(rect.left + Math.min(rect.width / 2, Math.max(10, rect.width - 10))),
                    Math.floor(rect.top + Math.min(rect.height / 2, Math.max(10, rect.height - 10))),
                ]);
            }
        }
        return points;
    }

    function _topBlockingOverlay() {
        for (const [x, y] of _collectOverlayProbePoints()) {
            const topEl = document.elementFromPoint(x, y);
            if (!topEl) continue;
            const blocking = topEl.closest?.(`#${_softBlockingOverlayIds.join(', #')}`);
            if (blocking && _isElementVisible(blocking)) return blocking;
        }
        return null;
    }

    function _forceUiUnlock(reason = 'auto-unfreeze') {
        const connected = !!(ws && ws.readyState === WebSocket.OPEN);
        const disclaimerAccepted = !!localStorage.getItem('ao_disclaimer_v2');
        if (!connected && !disclaimerAccepted) return;

        _hideSoftBlockingOverlays(reason);
        const blocking = _topBlockingOverlay();
        if (blocking) {
            blocking.style.display = 'none';
            blocking.setAttribute('aria-hidden', 'true');
            console.warn(`[ui] removed top blocking overlay: ${blocking.id || blocking.className} (${reason})`);
        }
    }

    function _scheduleUiUnlockSweep(reason = 'auto-unfreeze') {
        [0, 120, 400, 1200].forEach((delay) => {
            window.setTimeout(() => _forceUiUnlock(reason), delay);
        });
    }

    // Disclaimer flow → chains into setup wizard
    _bootstrapStartupGates();
    if (disclaimerAcceptBtn) disclaimerAcceptBtn.addEventListener('click', () => {
        localStorage.setItem('ao_disclaimer_v2', '1');
        if (disclaimerOverlay) disclaimerOverlay.style.display = 'none';
        _maybeShowSetupWizard();
    });
    if (disclaimerDeclineBtn) disclaimerDeclineBtn.addEventListener('click', async () => {
        disclaimerDeclineBtn.disabled = true;
        disclaimerDeclineBtn.textContent = 'Shutting down…';
        try { await authFetch('/api/shutdown', { method: 'POST' }); } catch (_) {}
        document.body.innerHTML = `
            <div style="display:flex;align-items:center;justify-content:center;height:100vh;background:#0f172a;color:#94a3b8;font-family:Inter,sans-serif;flex-direction:column;gap:16px">
                <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
                <p style="font-size:1.1rem;font-weight:600;color:#f1f5f9;margin:0">Access Declined</p>
                <p style="font-size:.875rem;margin:0">You declined the security notice. The server has been shut down.</p>
                <p style="font-size:.75rem;margin:0">You may close this tab.</p>
            </div>`;
    });

    if (policyBudgetModeSelect) {
        policyBudgetModeSelect.addEventListener('change', () => {
            appSettings.policy_budget_mode = _normalizePolicyBudgetMode(policyBudgetModeSelect.value);
            _updatePolicyBudgetModeUi();
            _renderBudgetCounter();
        });
    }

    if (settingsSaveBtn) settingsSaveBtn.addEventListener('click', async () => {
        await _saveSettingsPanel();
    });
    if (copilotEnabledSettingsToggle) copilotEnabledSettingsToggle.addEventListener('change', async () => {
        _syncCopilotEnabledToggles(copilotEnabledSettingsToggle.checked, 'settings');
        _refreshCopilotPanelUi();
        await _saveCopilotSettings();
    });
    if (copilotMorningBriefToggle) copilotMorningBriefToggle.addEventListener('change', _refreshCopilotPanelUi);
    if (copilotBriefingTimeInput) copilotBriefingTimeInput.addEventListener('input', _refreshCopilotPanelUi);
    if (copilotFollowupToggle) copilotFollowupToggle.addEventListener('change', _refreshCopilotPanelUi);
    if (copilotFollowupTimeInput) copilotFollowupTimeInput.addEventListener('input', _refreshCopilotPanelUi);
    if (copilotAutonomyLevelInput) copilotAutonomyLevelInput.addEventListener('change', _refreshCopilotPanelUi);
    if (copilotMorningInstructionsInput) copilotMorningInstructionsInput.addEventListener('input', _refreshCopilotPanelUi);
    if (copilotFollowupInstructionsInput) copilotFollowupInstructionsInput.addEventListener('input', _refreshCopilotPanelUi);
    if (copilotQuickstartSelect) copilotQuickstartSelect.addEventListener('change', _refreshCopilotPanelUi);
    if (copilotQuickstartBtn) copilotQuickstartBtn.addEventListener('click', () => _runSelectedCopilotQuickstart('footer'));
    if (copilotFollowupRunBtn) copilotFollowupRunBtn.addEventListener('click', () => _runCopilotById('follow-up', 'settings'));
    _settingsIntegrationsModule.bindIntegrationHandlers();

    let ws;
    let totalSteps = 0;
    let currentSessionId = null;
    let _preferredConversationSessionId = '';
    let _freshChatRequested = false;
    let _pendingSessionBootstrap = false;
    let _queuedBootstrapFollowUps = [];
    let recalledMemoryRaw = '';
    let _pendingEmailReaderRow = null;
    let _emailReaderModal = null;
    let _followupEmailRow = null;
    let _draftReplyPending = false;
    let _draftReplyRetryCount = 0;
    let _draftReplyRequest = null;

    let _lastStatusMessage = '';
    let _streamEntry = null;   // active streaming log-entry DOM element
    let _streamBody  = null;   // <span> receiving streamed tokens
    let _lastStreamEntry = null; // last completed stream entry (for stream_clear)
    let _thinkingBody = null;  // <pre> receiving thinking tokens
    let _thinkingDetails = null; // <details> wrapper for thinking block

    // ── Multi-run swim-lane management ───────────────────────────────────────────────
    const runs = new Map(); // run_id → {goal, lane, logPane, agentsEl, planEl, planList, progressWrap, progressLabel, progressFill, totalSteps, _streamEntry, _streamBody}
    let activeRunId = '';
    let _pendingPreRunChatBubbles = [];
    const _pendingRunSessionIds = new Map();
    const _pendingAnonymousSessionIds = [];
    let _currentHandlingRunId = '';
    const _discardedRunIds = new Set();
    const runsContainer = document.getElementById('runs-container');
    const _sessionStateModule = window.AgentFrontendModules.createSessionStateModule({
        refs: {
            goalModeHint,
            chatSessionPill,
        },
        helpers: {
            findRunIdBySessionId: _findRunIdBySessionId,
            ensureChatLane: _ensureChatLane,
            setActiveRun: _setActiveRun,
        },
        state: {
            getCurrentSessionId: () => currentSessionId,
            setCurrentSessionId: (value) => { currentSessionId = value; },
            getPreferredConversationSessionId: () => _preferredConversationSessionId,
            setPreferredConversationSessionId: (value) => { _preferredConversationSessionId = value; },
            getFreshChatRequested: () => _freshChatRequested,
            setFreshChatRequested: (value) => { _freshChatRequested = !!value; },
            getPendingSessionBootstrap: () => _pendingSessionBootstrap,
            setPendingSessionBootstrap: (value) => { _pendingSessionBootstrap = !!value; },
            getQueuedBootstrapFollowUps: () => _queuedBootstrapFollowUps,
            setQueuedBootstrapFollowUps: (value) => { _queuedBootstrapFollowUps = Array.isArray(value) ? value : []; },
            getPendingRunSessionIds: () => _pendingRunSessionIds,
            getPendingAnonymousSessionIds: () => _pendingAnonymousSessionIds,
            getCurrentHandlingRunId: () => _currentHandlingRunId,
            getActiveRunId: () => activeRunId,
            getRuns: () => runs,
            getModelsLoading: () => _modelsLoading,
            getAvailableModels: () => availableModels,
            getWs: () => ws,
        },
    });
    const _persistSession = _sessionStateModule.persistSession;
    const _restoreSession = _sessionStateModule.restoreSession;
    const _parseComposerInput = _sessionStateModule.parseComposerInput;
    const _refreshComposerModeUi = _sessionStateModule.refreshComposerModeUi;
    const _setChatSessionPill = _sessionStateModule.setChatSessionPill;
    const _sessionIdForRun = _sessionStateModule.sessionIdForRun;
    const _bindConversationSession = _sessionStateModule.bindConversationSession;
    const _enqueueAnonymousSessionId = _sessionStateModule.enqueueAnonymousSessionId;
    const _takeAnonymousSessionId = _sessionStateModule.takeAnonymousSessionId;
    const _composerPreferredSessionId = _sessionStateModule.composerPreferredSessionId;
    const _findBindableLiveRunId = _sessionStateModule.findBindableLiveRunId;

    function _refreshWorkflowIndicator() {
        if (!workflowStatePill) return;
        const active = _activeRunCount();
        if (active > 0) {
            workflowStatePill.innerHTML = `Active (${active}) <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" style="margin-left:3px;vertical-align:-1px;opacity:.7"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>`;
            workflowStatePill.classList.add('workflow-active');
            workflowStatePill.classList.remove('workflow-idle');
            if (workflowParallelHint) {
                workflowParallelHint.textContent = 'Running. You can send another message in parallel.';
            }
        } else {
            workflowStatePill.textContent = 'Ready';
            workflowStatePill.classList.add('workflow-idle');
            workflowStatePill.classList.remove('workflow-active');
            if (pillStopDropdown) pillStopDropdown.style.display = 'none';
            if (workflowParallelHint) {
                workflowParallelHint.textContent = 'Simple questions get instant answers. Complex tasks use the full agent workflow.';
            }
        }
        _renderPillStopDropdown();
    }

    function _isRunActive(run) {
        if (!run || !run.lane) return false;
        return !run.lane.classList.contains('lane-done') && !run.lane.classList.contains('lane-error');
    }

    function _isRunLlmActive(run) {
        if (!run) return false;
        return !!run._llmActive;
    }

    function _syncStopButtonState() {
        // Stop button removed; pill dropdown handles stop now
    }

    function _resolveKillTargetRunId() {
        if (activeRunId && runs.has(activeRunId) && _isRunActive(runs.get(activeRunId)) && _isRunLlmActive(runs.get(activeRunId))) {
            return activeRunId;
        }
        for (const [rid, run] of runs.entries()) {
            if (_isRunActive(run) && _isRunLlmActive(run)) return rid;
        }
        return '';
    }

    function _formatRunStatus(run) {
        if (!run) return 'Unknown';
        if (run.lane.classList.contains('lane-paused')) return 'Paused';
        if (run._lastAgentState === 'active' && run._lastAgent) {
            return `Running \u2014 ${run._lastAgentLabel || run._lastAgent}`;
        }
        if (run._killRequested) return 'Stopping\u2026';
        if (!_isRunLlmActive(run)) return 'Waiting for LLM';
        return 'Running';
    }

    function _requestRunKill(run_id) {
        if (!run_id || !runs.has(run_id)) return;
        const run = runs.get(run_id);
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ action: 'stop', run_id }));
        }
        run._killRequested = true;
        run._killRequestedAt = Date.now();
        setStatus(`Stopping workflow ${String(run_id).slice(0, 8)}\u2026`, 'error');
        _renderPillStopDropdown();
    }

    function _finalizeRunStopped(run_id, asError = false, removeFromUi = false) {
        if (!run_id || !runs.has(run_id)) return;
        const run = runs.get(run_id);
        const wasKillRequested = !!run._killRequested;
        run._llmActive = false;
        run._killRequested = false;
        run.lane.classList.remove('lane-paused');
        run.lane.classList.add(asError ? 'lane-error' : 'lane-done');
        _updateRunTabState(run_id, asError ? 'error' : 'done');
        _refreshWorkflowIndicator();
        if (removeFromUi || wasKillRequested) {
            _discardRunLane(run_id, false);
        }
    }

    function _renderPillStopDropdown() {
        if (!pillStopList) return;
        const activeRuns = [];
        for (const [rid, run] of runs.entries()) {
            if (_isRunActive(run)) activeRuns.push({ rid, run });
        }
        pillStopList.innerHTML = '';
        if (activeRuns.length === 0) return;

        activeRuns.forEach(({ rid, run }) => {
            const row = document.createElement('div');
            row.className = 'pill-stop-row' + (rid === activeRunId ? ' active' : '');
            row.style.cursor = 'pointer';

            const goalEl = document.createElement('span');
            goalEl.className = 'pill-stop-goal';
            const goal = String(run.goal || rid).trim();
            goalEl.textContent = goal.length > 50 ? goal.slice(0, 47) + '\u2026' : goal;
            goalEl.title = goal;

            const statusEl = document.createElement('span');
            statusEl.className = 'pill-stop-status';
            statusEl.textContent = _formatRunStatus(run);

            const killBtn = document.createElement('button');
            killBtn.type = 'button';
            killBtn.className = 'pill-stop-kill';
            killBtn.textContent = 'Stop';
            killBtn.disabled = !!run._killRequested;
            killBtn.addEventListener('click', (ev) => {
                ev.stopPropagation();
                _requestRunKill(rid);
            });

            // Click row (not kill button) → switch to this workflow
            row.addEventListener('click', () => {
                _setActiveRun(rid);
                if (pillStopDropdown) pillStopDropdown.style.display = 'none';
            });

            row.appendChild(goalEl);
            row.appendChild(statusEl);
            row.appendChild(killBtn);
            pillStopList.appendChild(row);
        });

        if (pillStopAll) {
            pillStopAll.style.display = activeRuns.length > 1 ? '' : 'none';
        }
    }

    // Old function name kept for any remaining callers
    function _renderActiveWorkflowList() { _renderPillStopDropdown(); }

    // Toggle pill dropdown on pill click
    if (workflowStatePill) {
        workflowStatePill.addEventListener('click', () => {
            if (!pillStopDropdown) return;
            if (_activeRunCount() === 0) return;
            _renderPillStopDropdown();
            pillStopDropdown.style.display = pillStopDropdown.style.display === 'none' ? '' : 'none';
        });
    }
    // Stop All button
    if (pillStopAll) {
        pillStopAll.addEventListener('click', () => {
            for (const [rid, run] of runs.entries()) {
                if (_isRunActive(run) && !run._killRequested) {
                    _requestRunKill(rid);
                }
            }
        });
    }
    // Close dropdown when clicking outside
    document.addEventListener('click', (e) => {
        if (pillStopDropdown && pillStopDropdown.style.display !== 'none') {
            if (!pillStopWrapper || !pillStopWrapper.contains(e.target)) {
                pillStopDropdown.style.display = 'none';
            }
        }
    });

    let _appSessionModule = null;

    function _callSessionModule(name, fallback, ...args) {
        const fn = _appSessionModule && _appSessionModule[name];
        return fn ? fn(...args) : fallback;
    }

    function _formatCompactNumber(...args) { return _callSessionModule('formatCompactNumber', '0', ...args); }
    function _normalizePolicyBudgetMode(...args) { return _callSessionModule('normalizePolicyBudgetMode', 'budget', ...args); }
    function _updatePolicyBudgetModeUi(...args) { return _callSessionModule('updatePolicyBudgetModeUi', undefined, ...args); }
    function _renderBudgetCounter(...args) { return _callSessionModule('renderBudgetCounter', undefined, ...args); }
    function _createPlanStepItem(...args) { return _callSessionModule('createPlanStepItem', null, ...args); }
    function _setPlanStepState(...args) { return _callSessionModule('setPlanStepState', undefined, ...args); }
    function _setPlanStepDetail(...args) { return _callSessionModule('setPlanStepDetail', undefined, ...args); }
    function _getPlanStepAt(...args) { return _callSessionModule('getPlanStepAt', null, ...args); }
    function _applyTodoProgress(...args) { return _callSessionModule('applyTodoProgress', undefined, ...args); }
    function showToast(...args) { return _callSessionModule('showToast', undefined, ...args); }
    function renderReportBody(...args) { return _callSessionModule('renderReportBody', '', ...args); }
    function formatFinalReport(...args) { return _callSessionModule('formatFinalReport', '', ...args); }
    function loadSessions(...args) { return _callSessionModule('loadSessions', undefined, ...args); }
    function _getViewedHistorySessionId(...args) { return _callSessionModule('getViewedHistorySessionId', '', ...args); }
    function _setViewedHistorySession(...args) { return _callSessionModule('setViewedHistorySession', undefined, ...args); }
    function _activateContinueSession(...args) { return _callSessionModule('activateContinueSession', undefined, ...args); }
    function renderSessions(...args) { return _callSessionModule('renderSessions', undefined, ...args); }
    function _laneLog(...args) { return _callSessionModule('laneLog', undefined, ...args); }
    function _fetchSession(id) {
        if (!_appSessionModule) _initAppSessionModule();
        if (_appSessionModule && typeof _appSessionModule.fetchSession === 'function') {
            return _appSessionModule.fetchSession(id);
        }
        return authFetch(`${BACKEND_URL}/api/sessions/${id}`)
            .then(r => {
                if (!r.ok) throw new Error(`HTTP ${r.status}`);
                return r.json();
            });
    }
    function replaySessionLane(...args) { return _callSessionModule('replaySessionLane', undefined, ...args); }
    function deleteSession(...args) { return _callSessionModule('deleteSession', undefined, ...args); }
    function setAgentState(...args) { return _callSessionModule('setAgentState', undefined, ...args); }
    function flashConnector(...args) { return _callSessionModule('flashConnector', undefined, ...args); }
    function setStatus(...args) { return _callSessionModule('setStatus', undefined, ...args); }
    function updateProgressBar(...args) { return _callSessionModule('updateProgressBar', undefined, ...args); }
    function _logTimestamp(...args) { return _callSessionModule('logTimestamp', document.createElement('span'), ...args); }
    function appendMemoryRecallLog(...args) { return _callSessionModule('appendMemoryRecallLog', undefined, ...args); }
    function appendLog(...args) { return _callSessionModule('appendLog', undefined, ...args); }
    function updateMemory(...args) { return _callSessionModule('updateMemory', undefined, ...args); }
    function showClarifyPrompt(...args) { return _callSessionModule('showClarifyPrompt', undefined, ...args); }

    const _chatRenderingModule = window.AgentFrontendModules.createChatRenderingModule({
        refs: {
            chatThread,
            chatThreadEmpty,
        },
        showFlowInThread: CHAT_SHOW_FLOW_IN_THREAD,
        helpers: {
            escapeHtml,
            formatFinalReport,
            authFetch,
            getBackendUrl: () => BACKEND_URL,
            showToast,
        },
        state: {
            getRuns: () => runs,
            getActiveRunId: () => activeRunId,
            getCurrentHandlingRunId: () => _currentHandlingRunId,
            getPendingPreRunChatBubbles: () => _pendingPreRunChatBubbles,
            getAppSettings: () => appSettings,
            getThinkingState: () => ({ body: _thinkingBody, details: _thinkingDetails }),
            setThinkingState: (body, details) => {
                _thinkingBody = body;
                _thinkingDetails = details;
            },
        },
    });
    const _ensureChatThreadVisibleState = _chatRenderingModule.ensureChatThreadVisibleState;
    const _compactChatThreadLayout = _chatRenderingModule.compactChatThreadLayout;
    const _scrollChatToBottom = _chatRenderingModule.scrollChatToBottom;
    const _forceStickChatBottom = _chatRenderingModule.forceStickChatBottom;
    const _renderChatMarkdown = _chatRenderingModule.renderChatMarkdown;
    const _buildChatBubbleDOM = _chatRenderingModule.buildChatBubbleDOM;
    const _shouldKeepThinkingExpanded = _chatRenderingModule.shouldKeepThinkingExpanded;
    const _ensureActiveThinkingDetails = _chatRenderingModule.ensureActiveThinkingDetails;
    const _buildReportBubbleDOM = _chatRenderingModule.buildReportBubbleDOM;
    const _appendChatBubble = _chatRenderingModule.appendChatBubble;
    const _appendReportBubble = _chatRenderingModule.appendReportBubble;
    const _showThinkingBubble = _chatRenderingModule.showThinkingBubble;
    const _removeThinkingBubble = _chatRenderingModule.removeThinkingBubble;
    const _setThinkingActivityState = _chatRenderingModule.setThinkingActivityState;
    const _normalizeThinkingText = _chatRenderingModule.normalizeThinkingText;
    const _appendProcessThinkingLine = _chatRenderingModule.appendProcessThinkingLine;
    const _appendAgentActivityBubble = _chatRenderingModule.appendAgentActivityBubble;
    const _buildPlanBubbleDOM = _chatRenderingModule.buildPlanBubbleDOM;
    const _appendPlanBubble = _chatRenderingModule.appendPlanBubble;
    const _buildStepBubbleDOM = _chatRenderingModule.buildStepBubbleDOM;
    const _appendStepBubble = _chatRenderingModule.appendStepBubble;
    const _formatLiveStepResult = _chatRenderingModule.formatLiveStepResult;
    const _normalizeWorkflowTextForCompare = _chatRenderingModule.normalizeWorkflowTextForCompare;
    const _isDuplicateWorkflowResult = _chatRenderingModule.isDuplicateWorkflowResult;
    const _finalizeStepBubbles = _chatRenderingModule.finalizeStepBubbles;
    _chatRenderingModule.bindUi();

    const _EMPTY_STATE_HTML = '<div class="chat-empty-icon"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div><p class="chat-empty-title">How can I help?</p><p class="chat-empty-sub">Ask a question or give an instruction — the system decides the best approach.</p><div class="chat-empty-chips"><button type="button" class="chat-empty-chip" data-query="What\'s on my calendar today?">📅 Today\'s agenda</button><button type="button" class="chat-empty-chip" data-query="Summarize my unread emails">📧 Unread mail</button><button type="button" class="chat-empty-chip" data-query="What\'s up? Give me the latest news and updates that match my interests">📰 What\'s up?</button><button type="button" class="chat-empty-chip" data-query="Create follow-up reminders for my open tasks">📋 Follow-up</button></div>';

    function _clearChatThread(showHint = true) {
        if (!chatThread) return;
        chatThread.classList.remove('has-messages');
        chatThread.querySelectorAll('.chat-bubble').forEach(node => node.remove());
        chatThread.querySelectorAll('.briefing-feedback-bar').forEach(node => node.remove());
        _removeThinkingBubble();
        if (chatThreadEmpty) chatThreadEmpty.style.display = showHint ? '' : 'none';
        if (showHint && chatThreadEmpty) {
            chatThreadEmpty.innerHTML = _EMPTY_STATE_HTML;
        }
    }

    function _renderChatForActiveRun() {
        if (!chatThread) return;
        _clearChatThread(false);
        const run = activeRunId && runs.has(activeRunId) ? runs.get(activeRunId) : null;
        if (!run || !run._chatBubbles.length) {
            if (chatThreadEmpty) {
                chatThreadEmpty.style.display = '';
                chatThreadEmpty.innerHTML = _EMPTY_STATE_HTML;
            }
            return;
        }
        if (chatThreadEmpty) chatThreadEmpty.style.display = 'none';
        if (!CHAT_SHOW_FLOW_IN_THREAD) {
            run._chatBubbles = (run._chatBubbles || []).filter((b) => {
                const t = b?.type || 'chat';
                if (t === 'plan' || t === 'step') return false;
                if (t === 'chat' && b?.role === 'system') {
                    const tx = String(b?.text || '').toLowerCase();
                    if (tx.includes('workflow started') || tx.includes('workflow completed')) return false;
                }
                return true;
            });
        }
        for (const b of run._chatBubbles) {
            const t = b.type || 'chat';
            if (!CHAT_SHOW_FLOW_IN_THREAD && (t === 'plan' || t === 'step')) continue;
            if (t === 'plan') {
                _buildPlanBubbleDOM(b.steps);
            } else if (t === 'step') {
                _buildStepBubbleDOM(b.stepIndex, b.totalSteps, b.description, true);
            } else if (t === 'report') {
                _buildReportBubbleDOM(b.goalText, b.content, b.outputFiles || []);
            } else {
                _buildChatBubbleDOM(b.role, b.text, b.meta);
            }
        }
        _scrollChatToBottom();
    }

    function _startNewChat() {
        currentSessionId = null;
        _preferredConversationSessionId = '';
        _persistSession(null);
        _freshChatRequested = true;
        _pendingSessionBootstrap = false;
        _queuedBootstrapFollowUps = [];
        _pendingPreRunChatBubbles = [];
        _currentHandlingRunId = '';
        activeRunId = '';
        _setViewedHistorySession('');
        _setChatSessionPill('');
        _clearChatThread(true);
        const tabsEl = document.getElementById('run-tabs');
        if (tabsEl) {
            tabsEl.querySelectorAll('.run-tab').forEach(tab => tab.classList.remove('active'));
        }
        for (const [, run] of runs) {
            if (run.lane) run.lane.style.display = 'none';
        }
        if (memoryContainer) memoryContainer.innerHTML = '';
        if (memoryCount) memoryCount.textContent = '';
        if (logContainer) logContainer.innerHTML = '';
        _renderBudgetCounter('');
        // Clear any chat lanes
        for (const [rid, run] of runs) {
            if (rid.startsWith('chat_')) _discardRunLane(rid);
        }
        if (goalInput) { goalInput.value = ''; _refreshComposerModeUi(); }
        // Clear context for new chat
        if (contextInput) contextInput.value = '';
        // Clear file attachments
        if (fileInput) { fileInput.value = ''; fileLabel.textContent = 'Attach files'; }
        if (folderPathInput) { folderPathInput.value = ''; }
        if (folderClearBtn) folderClearBtn.style.display = 'none';
        _refreshAttachChips();
        if (goalInput) goalInput.focus();
    }

    const newChatBtn = document.getElementById('new-chat-btn');
    if (newChatBtn) newChatBtn.addEventListener('click', _startNewChat);

    function _renderSessionChatHistory(session) {
        const chatHistory = Array.isArray(session?.chat_history) ? session.chat_history : [];
        const steps = Array.isArray(session?.steps) ? session.steps : [];
        const hasWorkflow = steps.length > 0;
        const hasFinalReport = !!(session?.final_report);
        const duplicateFinalReport = _isDuplicateWorkflowResult(session?.final_report, steps);
        const normalizedFinalReport = hasFinalReport
            ? _normalizeWorkflowTextForCompare(session?.final_report)
            : '';
        const hasContent = !!String(session?.goal || '').trim() || hasWorkflow || hasFinalReport || chatHistory.length > 0;
        const _previousHandlingRunId = _currentHandlingRunId;
        const _targetReplayRunId = activeRunId && runs.has(activeRunId) ? activeRunId : '';

        if (_targetReplayRunId) {
            _currentHandlingRunId = _targetReplayRunId;
            const _targetRun = runs.get(_targetReplayRunId);
            if (_targetRun) {
                _targetRun._chatBubbles = [];
            }
        }

        try {
            _clearChatThread(false);

            if (!hasContent) {
                _clearChatThread(true);
                if (chatThreadEmpty) {
                    chatThreadEmpty.innerHTML = _EMPTY_STATE_HTML;
                }
                return;
            }

            // 1. Show the original goal as a user message
            if (session?.goal) {
                _appendChatBubble('user', session.goal);
            }

            // 2. For workflows: show plan, then each step with result
            if (hasWorkflow) {
                const dateStr = session.created_at ? new Date(session.created_at).toLocaleString() : '';
                const modelStr = session.model || '';
                _appendChatBubble('system', `Workflow started${dateStr ? ` · ${dateStr}` : ''}${modelStr ? ` · ${modelStr}` : ''}`);

                // When there is a final report, collapse step details so they don't duplicate
                if (hasFinalReport) {
                    // Show a compact collapsible step summary instead of full step results
                    const completedSteps = steps.length;
                    const issueCount = steps.filter(s => (s.verdict || '').toUpperCase().startsWith('ISSUE')).length;
                    const _stepSummaryBubble = document.createElement('div');
                    _stepSummaryBubble.className = 'chat-bubble system';
                    const _stepSummaryContent = document.createElement('div');
                    _stepSummaryContent.className = 'chat-bubble-content';
                    const _stepSummaryDetails = document.createElement('details');
                    _stepSummaryDetails.className = 'workflow-steps-summary';
                    const _stepSummarySummary = document.createElement('summary');
                    _stepSummarySummary.className = 'chat-bubble-text';
                    _stepSummarySummary.textContent = `✓ ${completedSteps} step${completedSteps !== 1 ? 's' : ''} completed${issueCount > 0 ? ` · ${issueCount} with issues` : ''} — click to expand`;
                    _stepSummaryDetails.appendChild(_stepSummarySummary);
                    const _stepList = document.createElement('ol');
                    _stepList.className = 'workflow-history-steps';
                    steps.forEach(s => {
                        const li = document.createElement('li');
                        const isError = (s.verdict || '').toUpperCase().startsWith('FAIL') || (s.verdict || '').toUpperCase().startsWith('ISSUE');
                        li.textContent = (isError ? '⚠ ' : '✓ ') + (s.step || '');
                        if (isError) li.style.color = 'var(--danger)';
                        _stepList.appendChild(li);
                    });
                    _stepSummaryDetails.appendChild(_stepList);
                    _stepSummaryContent.appendChild(_stepSummaryDetails);
                    _stepSummaryBubble.appendChild(_stepSummaryContent);
                    if (chatThread) chatThread.appendChild(_stepSummaryBubble);
                } else {
                    _appendPlanBubble(steps.map(s => s.step || s));

                    // No final report ― show intermediate step results
                    steps.forEach((s, i) => {
                        _buildStepBubbleDOM(i, steps.length, s.step || '', true);
                        const result = (s.result || '').trim();
                        if (result) {
                            const isError = (s.verdict || '').toUpperCase().startsWith('FAIL') || (s.verdict || '').toUpperCase().startsWith('ISSUE');
                            _appendChatBubble(isError ? 'system' : 'app', result);
                        }
                    });

                    const completedSteps = steps.length;
                    const issueCount = steps.filter(s => (s.verdict || '').toUpperCase().startsWith('ISSUE')).length;
                    const summaryParts = [`${completedSteps} step${completedSteps !== 1 ? 's' : ''} completed`];
                    if (issueCount > 0) summaryParts.push(`${issueCount} with issues`);
                    _appendChatBubble('system', `✓ ${summaryParts.join(', ')}.`);
                }
            }

            // 3. Show the final report inline in chat (use rich report bubble, same as live runs)
            if (hasFinalReport) {
                _appendReportBubble(session.goal || '', session.final_report, session.output_files || []);
                // Briefing feedback buttons (#12)
                const _goal = (session.goal || '').toLowerCase();
                const _isBriefing = _goal.includes('ochtendbrief') || _goal.includes('morning brief')
                    || _goal.includes('follow-up') || _goal.includes('opvolgbrief');
                if (_isBriefing && chatThread) {
                    const _feedbackBar = document.createElement('div');
                    _feedbackBar.className = 'briefing-feedback-bar';
                    const _bType = (_goal.includes('follow') || _goal.includes('opvolg')) ? 'followup' : 'morning';
                    _feedbackBar.innerHTML = '<span style="opacity:.6;font-size:.8rem">Was this briefing helpful?</span>'
                        + ' <button class="briefing-fb-btn" data-rating="thumbs_up" title="Helpful">👍</button>'
                        + ' <button class="briefing-fb-btn" data-rating="thumbs_down" title="Not helpful">👎</button>';
                    _feedbackBar.querySelectorAll('.briefing-fb-btn').forEach(btn => {
                        btn.addEventListener('click', async () => {
                            try {
                                await fetch('/api/copilot/feedback', {
                                    method: 'POST',
                                    headers: {'Content-Type': 'application/json'},
                                    body: JSON.stringify({ type: _bType, rating: btn.dataset.rating })
                                });
                                _feedbackBar.innerHTML = '<span style="opacity:.6;font-size:.8rem">Thanks for your feedback!</span>';
                            } catch(e) { console.warn('Feedback send failed', e); }
                        });
                    });
                    chatThread.appendChild(_feedbackBar);
                }
            }

            // 4. Show follow-up chat history
            const _goalText = String(session?.goal || '').trim();
            const _goalBare = _goalText.replace(/^Chat:\s*/i, '');
            chatHistory.forEach((item, _idx) => {
                const role = String(item?.role || '').toLowerCase();
                const content = String(item?.content || '').trim();
                if (!content) return;
                // Skip first user entry if it duplicates the goal already shown above
                if (_idx === 0 && role === 'user' && (content === _goalText || content === _goalBare)) return;
                if (role !== 'user' && normalizedFinalReport && _normalizeWorkflowTextForCompare(content) === normalizedFinalReport) return;
                if (role === 'user') {
                    _appendChatBubble('user', content);
                } else {
                    _appendChatBubble('app', content);
                }
            });
        } finally {
            _currentHandlingRunId = _previousHandlingRunId;
        }
    }

    function _mirrorLogToDialog(message, type, agent = null) {
        const text = String(message || '').trim();
        if (!text) return;
        // _appendChatBubble handles storage on correct run + display scoping
        const normalizedAgent = String(agent || '').trim().toLowerCase();
        const isUser = normalizedAgent === 'you' || normalizedAgent === 'user' || normalizedAgent === 'gebruiker';
        const shouldMirrorApp =
            normalizedAgent === 'chat' ||
            normalizedAgent === 'workflow' ||
            (String(type || '').toLowerCase() === 'error');

        if (!isUser && !shouldMirrorApp) {
            return;
        }

        const role = isUser ? 'user' : 'app';
        const meta = isUser
            ? ''
            : (agent ? String(agent) : String(type || '').toUpperCase());
        _appendChatBubble(role, text, meta);
    }

    function _resolveSessionForRun(runId) {
        const runSessionId = _sessionIdForRun(runId);
        if (runSessionId) return runSessionId;
        return currentSessionId || _preferredConversationSessionId || null;
    }

    function _isHistoryRunId(runId) {
        return String(runId || '').startsWith('hist_');
    }

    function _findRunIdBySessionId(sessionId, includeHistory = true) {
        const target = String(sessionId || '').trim();
        if (!target) return '';
        for (const [rid, run] of runs.entries()) {
            if (!includeHistory && _isHistoryRunId(rid)) continue;
            if (String(run?._sessionId || '').trim() === target) {
                return rid;
            }
        }
        for (const [rid, pendingSessionId] of _pendingRunSessionIds.entries()) {
            if (!includeHistory && _isHistoryRunId(rid)) continue;
            if (String(pendingSessionId || '').trim() === target) {
                return rid;
            }
        }
        return '';
    }

    function _ensureChatLane(sessionId) {
        const targetSessionId = String(sessionId || '').trim();
        const existingRunId = _findRunIdBySessionId(targetSessionId, false);
        if (existingRunId && runs.has(existingRunId)) {
            _setActiveRun(existingRunId);
            return existingRunId;
        }
        if (activeRunId && !_isHistoryRunId(activeRunId) && runs.has(activeRunId)) {
            const activeSessionId = _sessionIdForRun(activeRunId);
            if (!activeSessionId || activeSessionId === targetSessionId) {
                if (!activeSessionId && targetSessionId) {
                    _bindConversationSession(targetSessionId, activeRunId, false);
                }
                _setActiveRun(activeRunId);
                return activeRunId;
            }
        }
        let rid = `chat_${String(targetSessionId || Date.now().toString(36)).slice(0, 8)}`;
        if (runs.has(rid) && _sessionIdForRun(rid) && _sessionIdForRun(rid) !== targetSessionId) {
            rid = `chat_${Date.now().toString(36)}`;
        }
        if (!runs.has(rid)) {
            _createRunLane(rid, '[Chat] Follow-up');
            const lane = runs.get(rid);
            if (lane) {
                lane._sessionId = targetSessionId || null;
                lane.lane.classList.add('lane-done');
            }
        }
        if (targetSessionId) {
            _bindConversationSession(targetSessionId, rid, false);
        }
        _setActiveRun(rid);
        return rid;
    }

    /** Count lanes that are still running or paused (not yet done/error) */
    function _activeRunCount() {
        let n = 0;
        runs.forEach(r => {
            if (!r.lane.classList.contains('lane-done') && !r.lane.classList.contains('lane-error')) n++;
        });
        return n;
    }

    /** Count lanes that are actively in a model call right now */
    function _activeLlmRunCount() {
        let n = 0;
        runs.forEach(r => {
            if (_isRunActive(r) && _isRunLlmActive(r)) n++;
        });
        return n;
    }

    const AGENT_ICONS = {
        planner: '\ud83d\udd75\ufe0f', researcher: '\ud83e\uddd1\u200d\ud83d\udcbb', curator: '\ud83e\uddd1\u200d\ud83c\udfeb',
        executor: '\ud83e\uddd1\u200d\ud83d\udd27', validator: '\ud83e\uddd1\u200d\u2696\ufe0f', reportout: '\ud83e\uddd1\u200d\ud83d\udcbc'
    };
    const AGENT_ORDER = ['planner', 'researcher', 'curator', 'executor', 'validator', 'reportout'];

    function _getRunContainer(run_id) {
        if (run_id && runs.has(run_id)) return runs.get(run_id).logPane;
        // fallback: use the first lane's log, or the hidden logContainer
        if (runs.size > 0) return runs.values().next().value.logPane;
        return logContainer;
    }

    function _discardRunLane(run_id, sendStop = true) {
        _discardedRunIds.add(run_id);
        _pendingRunSessionIds.delete(run_id);
        // Stop the run on the server if it's still active
        if (sendStop && ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ action: 'stop', run_id }));
        }
        const run = runs.get(run_id);
        if (run) {
            run.lane.remove();
            runs.delete(run_id);
        }
        _removeRunTab(run_id);
        if (runs.size === 0 && runsContainer) {
            const hint = document.createElement('div');
            hint.className = 'runs-empty-hint';
            hint.textContent = 'Start an agent workflow to begin';
            runsContainer.appendChild(hint);
        }
        _updateLaneLayout();
        _refreshWorkflowIndicator();
        if (activeRunId === run_id) {
            const nextId = runs.size > 0 ? runs.keys().next().value : '';
            if (nextId) {
                _setActiveRun(nextId);
            } else {
                activeRunId = '';
                if (memoryContainer) memoryContainer.innerHTML = '';
                if (memoryCount) memoryCount.textContent = '';
                if (logContainer) logContainer.innerHTML = '';
                    setStatus('Connected to server', 'success');
                _renderBudgetCounter('');
            }
        }
    }

    function _updateLaneLayout() {
        if (runsContainer) runsContainer.dataset.lanes = runs.size;
    }

    function _setActiveRun(run_id) {
        if (!run_id || !runs.has(run_id)) return;
        activeRunId = run_id;
        const _isHistoryRun = String(run_id || '').startsWith('hist_');
        if (!_isHistoryRun) {
            _freshChatRequested = false;
        }
        // Sync currentSessionId from this run's session
        const _activeRun = runs.get(run_id);
        const _activeRunSessionId = _sessionIdForRun(run_id);
        if (!_isHistoryRun && _activeRunSessionId) {
            currentSessionId = _activeRunSessionId;
            _preferredConversationSessionId = _activeRunSessionId;
            _persistSession(currentSessionId);
            _setChatSessionPill(currentSessionId);
        }
        _renderBudgetCounter(run_id);
        // Show only the active lane, hide the rest
        for (const [rid, r] of runs) {
            r.lane.style.display = rid === run_id ? '' : 'none';
        }
        // Update tab active state
        const tabsEl = document.getElementById('run-tabs');
        if (tabsEl) {
            tabsEl.querySelectorAll('.run-tab').forEach(t => {
                t.classList.toggle('active', t.dataset.runId === run_id);
            });
        }
        // Re-render chat thread for this run's session
        _renderChatForActiveRun();
    }

    function _addRunTab(run_id, goal) {
        const tabsEl = document.getElementById('run-tabs');
        if (!tabsEl) return;
        // Don't add duplicate
        if (tabsEl.querySelector(`[data-run-id="${run_id}"]`)) return;
        const tab = document.createElement('button');
        tab.className = 'run-tab';
        tab.dataset.runId = run_id;
        const shortGoal = (goal || run_id).length > 30 ? (goal || run_id).slice(0, 28) + '…' : (goal || run_id);
        tab.textContent = shortGoal;
        tab.title = goal || run_id;
        tab.addEventListener('click', () => _setActiveRun(run_id));
        tabsEl.appendChild(tab);
        tabsEl.classList.toggle('has-multiple-tabs', tabsEl.children.length > 1);
    }

    function _removeRunTab(run_id) {
        const tabsEl = document.getElementById('run-tabs');
        if (!tabsEl) return;
        const tab = tabsEl.querySelector(`[data-run-id="${run_id}"]`);
        if (tab) tab.remove();
        tabsEl.classList.toggle('has-multiple-tabs', tabsEl.children.length > 1);
    }

    function _updateRunTabState(run_id, state) {
        const tabsEl = document.getElementById('run-tabs');
        if (!tabsEl) return;
        const tab = tabsEl.querySelector(`[data-run-id="${run_id}"]`);
        if (!tab) return;
        tab.classList.remove('done', 'error');
        if (state === 'done') tab.classList.add('done');
        else if (state === 'error') tab.classList.add('error');
    }

    function _initAppSessionModule() {
        if (_appSessionModule) return;
        const _appSessionScope = {};
        Object.defineProperties(_appSessionScope, {
            BACKEND_URL: { get: () => BACKEND_URL, set: (value) => { BACKEND_URL = value; }, enumerable: true },
            appSettings: { get: () => appSettings, set: (value) => { appSettings = value; }, enumerable: true },
            currentSessionId: { get: () => currentSessionId, set: (value) => { currentSessionId = value; }, enumerable: true },
            _preferredConversationSessionId: { get: () => _preferredConversationSessionId, set: (value) => { _preferredConversationSessionId = value; }, enumerable: true },
            _freshChatRequested: { get: () => _freshChatRequested, set: (value) => { _freshChatRequested = !!value; }, enumerable: true },
            _currentHandlingRunId: { get: () => _currentHandlingRunId, set: (value) => { _currentHandlingRunId = value; }, enumerable: true },
            activeRunId: { get: () => activeRunId, set: (value) => { activeRunId = value; }, enumerable: true },
            recalledMemoryRaw: { get: () => recalledMemoryRaw, set: (value) => { recalledMemoryRaw = value; }, enumerable: true },
            totalSteps: { get: () => totalSteps, set: (value) => { totalSteps = value; }, enumerable: true },
            ws: { get: () => ws, set: (value) => { ws = value; }, enumerable: true },
        });
        Object.assign(_appSessionScope, {
            authFetch,
            authFetchJson,
            sessionsContainer,
            schedulesContainer,
            statusChip,
            statusText,
            memoryContainer,
            memoryCount,
            logContainer,
            goalInput,
            budgetCount,
            budgetCounterEl,
            tokenCount,
            tokenCounterEl,
            policyBudgetModeSelect,
            policyBudgetModeNote,
            policyMaxStepsInput,
            policyMaxToolCallsInput,
            policyMaxTokensInput,
            policyMaxRuntimeInput,
            runs,
            runsContainer,
            escapeHtml,
            _linkifyUrls,
            _loadSchedules,
            _formatInterval,
            _persistSession,
            _setChatSessionPill,
            _findRunIdBySessionId,
            _setActiveRun,
            _ensureChatLane,
            _renderSessionChatHistory,
            _showSchedulePopover,
            toggleSchedule,
            removeSchedule,
            sendStart,
            _appendChatBubble,
            AGENT_ORDER,
            AGENT_ICONS,
            _discardRunLane,
            _updateLaneLayout,
            _addRunTab,
            showReportModal,
            _refreshWorkflowIndicator,
            _updateRunTabState,
            _mirrorLogToDialog,
            _getRunContainer,
        });
        _appSessionModule = window.AgentFrontendModules.createSessionHistoryModule(_appSessionScope);
    }

    function _showAgentSuggestBubble(suggestGoal, customText) {
        if (!chatThread) return;
        // Remove previous suggest bubbles
        chatThread.querySelectorAll('.suggest-agent-bubble').forEach(el => el.remove());

        const bubble = document.createElement('div');
        bubble.className = 'chat-bubble suggest-agent-bubble';

        const avatar = document.createElement('div');
        avatar.className = 'chat-avatar';
        avatar.textContent = '💡';
        bubble.appendChild(avatar);

        const content = document.createElement('div');
        content.className = 'chat-bubble-content';
        const roleEl = document.createElement('div');
        roleEl.className = 'chat-bubble-role';
        roleEl.textContent = 'Suggestion';
        content.appendChild(roleEl);

        const textEl = document.createElement('div');
        textEl.className = 'chat-bubble-text suggest-text';
        textEl.textContent = customText || 'Would you like to start an agent workflow for this request?';
        content.appendChild(textEl);

        const btnRow = document.createElement('div');
        btnRow.className = 'suggest-btn-row';

        const yesBtn = document.createElement('button');
        yesBtn.className = 'suggest-agent-btn suggest-yes';
        yesBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Yes, start';
        yesBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const goal = suggestGoal;
            bubble.remove();
            // Force a fresh workflow — clear session so it doesn't re-route to chat
            currentSessionId = null;
            _setChatSessionPill('');
            if (goalInput) goalInput.value = goal;
            _refreshComposerModeUi();
            executeComposerAction('run');
        });

        const noBtn = document.createElement('button');
        noBtn.className = 'suggest-agent-btn suggest-no';
        noBtn.textContent = 'No';
        noBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            bubble.remove();
        });

        btnRow.appendChild(yesBtn);
        btnRow.appendChild(noBtn);
        content.appendChild(btnRow);

        bubble.appendChild(content);
        chatThread.appendChild(bubble);
        _scrollChatToBottom();
    }

    function showReportModal(goalText, content, outputFiles) {
        _buildReportBubbleDOM(goalText, content, outputFiles || []);
        _scrollChatToBottom();
    }

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

    if (goalInput) {
        goalInput.addEventListener('input', _refreshComposerModeUi);

        // Auto-resize textarea
        goalInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = Math.min(this.scrollHeight, 160) + 'px';
        });

        // Enter to send (Shift+Enter for newline)
        goalInput.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                executeComposerAction();
            }
        });
    }

    // --- Inline composer send button ---
    if (composerSendBtn) {
        composerSendBtn.addEventListener('click', () => executeComposerAction());
    }

    clearLogBtn.addEventListener('click', () => {
        // In lane mode, delete entire exploration flows (not only details/log text).
        if (runs.size > 0) {
            const runIds = Array.from(runs.keys());
            runIds.forEach(rid => _discardRunLane(rid));
            _streamEntry = null;
            _streamBody = null;
            setStatus('Exploration flows removed', 'idle');
            return;
        }
        const pane = _getRunContainer(activeRunId);
        pane.innerHTML = '';
        _streamEntry = null;
        _streamBody = null;
        setStatus('Exploration log cleared', 'idle');
    });

    function setHistoryCollapsed(collapsed) {
        if (!explorationHistory) return;
        explorationHistory.classList.toggle('history-collapsed', collapsed);
        if (appShell) {
            appShell.classList.toggle('sidebar-collapsed', collapsed);
        }
        if (explorationLayout) {
            explorationLayout.classList.toggle('history-collapsed', collapsed);
        }
        if (historyCollapseBtn) {
            historyCollapseBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
            historyCollapseBtn.title = collapsed ? 'Expand section' : 'Collapse section';
        }
        localStorage.setItem(HISTORY_COLLAPSED_STORAGE_KEY, collapsed ? '1' : '0');
    }

    if (historyCollapseBtn) {
        historyCollapseBtn.addEventListener('click', () => {
            const collapsed = !!explorationHistory?.classList.contains('history-collapsed');
            setHistoryCollapsed(!collapsed);
        });
    }

    // --- Tab switching (Memory / History / Schedules / Identity) ---
    tabs.forEach(btn => {
        btn.addEventListener('click', () => {
            tabs.forEach(t => t.classList.remove('active'));
            btn.classList.add('active');
            const tab = btn.dataset.tab;
            memoryContainer.classList.toggle('active', tab === 'current');
            memoryContainer.style.display   = tab === 'current'   ? 'flex' : 'none';
            sessionsContainer.classList.toggle('active', tab === 'sessions');
            sessionsContainer.style.display  = tab === 'sessions'  ? 'flex' : 'none';
            if (schedulesContainer) {
                schedulesContainer.classList.toggle('active', tab === 'schedules');
                schedulesContainer.style.display = tab === 'schedules' ? 'flex' : 'none';
            }
            identityContainer.classList.toggle('active', tab === 'identity');
            identityContainer.style.display  = tab === 'identity'  ? 'flex' : 'none';
            clearSessionsBtn.style.display   = tab === 'sessions'  ? 'inline-flex' : 'none';
            if (tab === 'sessions') loadSessions();
            if (tab === 'schedules') renderSchedules();
            if (tab === 'identity') loadIdentity();
        });
    });

    // Default collapsed unless user explicitly expanded (stored '0')
    const _histPref = localStorage.getItem(HISTORY_COLLAPSED_STORAGE_KEY);
    setHistoryCollapsed(_histPref === null ? true : _histPref === '1');

    const _browserPreviewPref = localStorage.getItem(BROWSER_PREVIEW_COLLAPSED_STORAGE_KEY);
    setBrowserPreviewCollapsed(_browserPreviewPref === null ? false : _browserPreviewPref === '1');

    // Default tab is now History — load sessions on startup
    loadSessions();
    clearSessionsBtn.style.display = 'inline-flex';

    // Show/hide clear-all button only on sessions tab (default hidden)
    clearSessionsBtn.addEventListener('click', async () => {
        if (!confirm('Delete all history?')) return;
        await authFetch(`${BACKEND_URL}/api/sessions`, { method: 'DELETE' });
        loadSessions();
    });

    // --- HITL approval modal ---
    let _pendingApprovalRunId = '';
    function showApprovalModal(stepIndex, stepDesc, runId = '') {
        _pendingApprovalRunId = runId || '';
        approvalStepNum.textContent  = stepIndex + 1;
        approvalStepDesc.textContent = stepDesc;
        approvalNote.value           = '';
        approvalOverlay.style.display = 'flex';
    }
    function hideApprovalModal() {
        _pendingApprovalRunId = '';
        approvalOverlay.style.display = 'none';
    }
    function sendApproval(response) {
        const note = approvalNote ? approvalNote.value.trim() : '';
        if (ws && ws.readyState === WebSocket.OPEN) {
            const payload = { action: 'approval_response', response, note };
            if (_pendingApprovalRunId) payload.run_id = _pendingApprovalRunId;
            ws.send(JSON.stringify(payload));
        }
        hideApprovalModal();
    }
    approvalApproveBtn.addEventListener('click', () => sendApproval('approve'));
    approvalAlwaysBtn.addEventListener('click',  () => sendApproval('approve_always'));
    approvalRejectBtn.addEventListener('click',  () => sendApproval('reject'));

    // --- Server Offline Overlay + Launcher Control (port 8001) ---
    _offlineOverlay     = document.getElementById('server-offline-overlay');
    _offlineSub         = document.getElementById('server-offline-sub');
    _launcherCloseBtn   = document.getElementById('launcher-close-btn');
    _launcherStartBtn   = document.getElementById('launcher-start-btn');
    _launcherRestartBtn = document.getElementById('launcher-restart-btn');
    _launcherHint       = document.getElementById('launcher-status-hint');
    let _launcherStatusSnapshot = null;
    let _clientHeartbeatTimer = null;
    let _recoveryWatchdogTimer = null;
    let _launcherSpawnPromise = null;
    let _lastLauncherSpawnAttemptAt = 0;
    const LAUNCHER_SPAWN_RETRY_MS = 15000;
    const RECOVERY_WATCHDOG_MS = 5000;

    // --- Stop-on-close preference (persisted in localStorage, default ON) ---
    const _stopOnCloseRaw = localStorage.getItem('ao_stop_on_close');
    let _stopOnClose = _stopOnCloseRaw === null || _stopOnCloseRaw === '1';   // default ON
    if (stopOnCloseToggle) {
        stopOnCloseToggle.checked = _stopOnClose;
        stopOnCloseToggle.addEventListener('change', () => {
            _stopOnClose = stopOnCloseToggle.checked;
            localStorage.setItem('ao_stop_on_close', _stopOnClose ? '1' : '0');
        });
    }

    /** True when this is the only frontend tab connected to the launcher. */
    function _isLastClient() {
        const n = Number(_launcherStatusSnapshot?.active_clients || 0);
        return n <= 1;
    }

    function _applyLauncherStatus(status) {
        const data = status && typeof status === 'object' ? status : null;
        _launcherStatusSnapshot = data;
        _launcherAvailable = !!data;
        if (!data) return;

        const state = String(data.state || (data.running ? 'running' : 'idle')).toLowerCase();
        const activeClients = Number(data.active_clients || 0);
        const canControl = state !== 'starting' && state !== 'stopping';
        if (_launcherStartBtn) _launcherStartBtn.disabled = !canControl;
        if (_launcherRestartBtn) _launcherRestartBtn.disabled = !canControl;
        if (!_launcherHint) return;

        if (state === 'running') {
            _launcherHint.textContent = activeClients > 1
                ? `Backend running — ${activeClients} frontend clients active.`
                : 'Backend running — connecting or syncing...';
            _launcherHint.className = 'server-offline-hint ok';
            // Backend is live but WS is disconnected — kick off a reconnect now
            // so the user doesn't have to wait for the backoff timer to fire.
            // Only act when the socket is fully closed (not while CONNECTING/CLOSING).
            if (!_pageClosing && (!ws || ws.readyState === WebSocket.CLOSED) && !_reconnectTimer) {
                _reconnectAttempt = 0;
                connectWebSocket();
            }
            return;
        }
        if (state === 'starting') {
            _launcherHint.textContent = 'Launcher active — backend starting, please wait...';
            _launcherHint.className = 'server-offline-hint busy';
            return;
        }
        if (state === 'stopping') {
            _launcherHint.textContent = 'Backend shutting down gracefully...';
            _launcherHint.className = 'server-offline-hint busy';
            return;
        }
        if (data.user_stopped) {
            _launcherHint.textContent = 'Backend stopped manually — click Start to restart.';
            _launcherHint.className = 'server-offline-hint ok';
            return;
        }
        if (data.last_error) {
            _launcherHint.textContent = `Launcher active, but backend unhealthy: ${data.last_error}`;
            _launcherHint.className = 'server-offline-hint err';
            return;
        }
        _launcherHint.textContent = 'Launcher available — backend starts automatically when the client is active.';
        _launcherHint.className = 'server-offline-hint ok';
    }

    async function _spawnLauncherViaBackend(options = {}) {
        const {
            reason = 'auto',
            force = false,
            updateHint = true,
        } = options;

        if (_launcherSpawnPromise) return _launcherSpawnPromise;
        const now = Date.now();
        if (!force && (now - _lastLauncherSpawnAttemptAt) < LAUNCHER_SPAWN_RETRY_MS) {
            return false;
        }
        _lastLauncherSpawnAttemptAt = now;

        _launcherSpawnPromise = (async () => {
            try {
                if (updateHint && _launcherHint) {
                    _launcherHint.textContent = 'Launcher niet bereikbaar — frontend start hem via de backend...';
                    _launcherHint.className = 'server-offline-hint busy';
                }
                await _refreshAuthToken({ preferDiscovery: true });
                const response = await fetch(_resolveApiUrl('/api/spawn-launcher'), {
                    method: 'POST',
                    signal: AbortSignal.timeout(15000),
                });
                let data = null;
                try {
                    data = await response.json();
                } catch (_) {
                    data = { ok: false, message: `HTTP ${response.status}` };
                }
                if (!response.ok || !data?.ok) {
                    if (updateHint && _launcherHint) {
                        _launcherHint.textContent = data?.message || 'Launcher spawn failed.';
                        _launcherHint.className = 'server-offline-hint err';
                    }
                    return false;
                }
                if (updateHint && _launcherHint) {
                    _launcherHint.textContent = 'Launcher gestart — status wordt opnieuw opgehaald...';
                    _launcherHint.className = 'server-offline-hint ok';
                }
                await _notifyLauncherClientState(`spawn:${reason}`);
                return true;
            } catch (_) {
                if (updateHint && _launcherHint) {
                    _launcherHint.textContent = 'Frontend kan de launcher niet starten omdat ook de backend niet bereikbaar is.';
                    _launcherHint.className = 'server-offline-hint err';
                }
                return false;
            } finally {
                _launcherSpawnPromise = null;
            }
        })();

        return _launcherSpawnPromise;
    }

    async function _notifyLauncherClientState(reason = 'heartbeat') {
        try {
            const response = await fetch(`${LAUNCHER_URL}/api/launcher/client/ping`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    client_id: _frontendClientId,
                    visible: document.visibilityState !== 'hidden',
                    reason,
                }),
                signal: AbortSignal.timeout(4000),
            });
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const data = await response.json();
            _applyLauncherStatus(data);
            return true;
        } catch (_) {
            _launcherAvailable = false;
            if (!_pageClosing) {
                void _spawnLauncherViaBackend({ reason, updateHint: _offlineShown });
            }
            return false;
        }
    }

    function _startRecoveryWatchdog() {
        if (_recoveryWatchdogTimer) clearInterval(_recoveryWatchdogTimer);
        _recoveryWatchdogTimer = setInterval(() => {
            if (_pageClosing) return;
            if (document.visibilityState === 'hidden') return;
            const wsClosed = !ws || ws.readyState === WebSocket.CLOSED;
            if (!_launcherAvailable || wsClosed) {
                void _notifyLauncherClientState('watchdog');
            }
            if (wsClosed && !_reconnectTimer) {
                connectWebSocket();
            }
        }, RECOVERY_WATCHDOG_MS);
    }

    function _disconnectLauncherClient(reason = 'pagehide') {
        const payload = JSON.stringify({ client_id: _frontendClientId, reason });
        if (navigator.sendBeacon) {
            try {
                navigator.sendBeacon(
                    `${LAUNCHER_URL}/api/launcher/client/disconnect`,
                    new Blob([payload], { type: 'application/json' }),
                );
                return;
            } catch (_) {}
        }
        fetch(`${LAUNCHER_URL}/api/launcher/client/disconnect`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: payload,
            keepalive: true,
        }).catch(() => {});
    }

    function _sendLauncherShutdownBeacon() {
        const payload = JSON.stringify({ client_id: _frontendClientId, reason: 'tab-close-shutdown' });
        if (navigator.sendBeacon) {
            try {
                navigator.sendBeacon(
                    `${LAUNCHER_URL}/api/launcher/shutdown`,
                    new Blob([payload], { type: 'application/json' }),
                );
                return;
            } catch (_) {}
        }
        fetch(`${LAUNCHER_URL}/api/launcher/shutdown`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: payload,
            keepalive: true,
        }).catch(() => {});
    }

    function _startClientHeartbeat() {
        if (_clientHeartbeatTimer) clearInterval(_clientHeartbeatTimer);
        _notifyLauncherClientState('init');
        _clientHeartbeatTimer = setInterval(() => {
            _notifyLauncherClientState('heartbeat');
        }, 15000);
    }

    /** Show the server-offline overlay (called after several failed reconnect attempts). */
    function _showOfflineOverlay() {
        if (_offlineShown || _offlineDismissed || !_offlineOverlay) return;
        _offlineShown = true;
        _offlineOverlay.style.display = 'flex';
        _pollLauncherStatus();
    }

    function _hideOfflineOverlay() {
        if (!_offlineOverlay) return;
        _offlineShown = false;
        _offlineDismissed = false;
        _offlineOverlay.style.display = 'none';
        if (_launcherPollTimer) { clearInterval(_launcherPollTimer); _launcherPollTimer = null; }
    }

    function _dismissOfflineOverlay() {
        if (!_offlineOverlay) return;
        _offlineShown = false;
        _offlineDismissed = true;
        _offlineOverlay.style.display = 'none';
        if (_launcherPollTimer) { clearInterval(_launcherPollTimer); _launcherPollTimer = null; }
    }

    document.addEventListener('keydown', (event) => {
        if (event.key !== 'Escape') return;
        if (ws && ws.readyState === WebSocket.OPEN) {
            _hideStartupBlockers();
            _forceUiUnlock('escape');
        }
    });

    /** Check if the launcher control server on port 8001 is reachable. */
    async function _pollLauncherStatus() {
        try {
            const r = await fetch(`${LAUNCHER_URL}/api/launcher/status`, { signal: AbortSignal.timeout(3000) });
            if (r.ok) {
                _applyLauncherStatus(await r.json());
            } else {
                throw new Error('bad status');
            }
        } catch (_) {
            _launcherAvailable = false;
            _launcherStatusSnapshot = null;
            if (_launcherHint) {
                _launcherHint.textContent = 'Launcher niet bereikbaar — frontend probeert hem via de backend te starten.';
                _launcherHint.className = 'server-offline-hint err';
            }
            void _spawnLauncherViaBackend({ reason: 'poll', updateHint: true });
            // Keep buttons enabled — _launcherAction will still fall back to backend /api/spawn-launcher
            if (_launcherStartBtn) _launcherStartBtn.disabled = false;
            if (_launcherRestartBtn) _launcherRestartBtn.disabled = false;
        }
        // Keep polling every 5s while overlay is visible
        if (_offlineShown && !_launcherPollTimer) {
            _launcherPollTimer = setInterval(_pollLauncherStatus, 5000);
        }
    }

    async function _waitForBackendReady(timeoutMs = 15000) {
        const deadline = Date.now() + timeoutMs;
        while (Date.now() < deadline) {
            if (await _refreshAuthToken({ preferDiscovery: true })) return true;
            await new Promise((resolve) => setTimeout(resolve, 800));
        }
        return false;
    }

    /** Send a command to the launcher control server (with backend fallback). */
    async function _launcherAction(action) {
        _offlineDismissed = false;
        if (_launcherHint) { _launcherHint.textContent = 'Working...'; _launcherHint.className = 'server-offline-hint busy'; }
        if (_launcherStartBtn) _launcherStartBtn.disabled = true;
        if (_launcherRestartBtn) _launcherRestartBtn.disabled = true;

        // --- Fallback: launcher unreachable → try spawning via backend ---
        if (!_launcherAvailable) {
            try {
                if (_launcherHint) { _launcherHint.textContent = 'Launcher offline — spawning via backend...'; _launcherHint.className = 'server-offline-hint busy'; }
                await _refreshAuthToken({ preferDiscovery: true });
                const fb = await fetch(_resolveApiUrl('/api/spawn-launcher'), {
                    method: 'POST',
                    signal: AbortSignal.timeout(15000),
                });
                const fbData = await fb.json();
                if (fbData.ok) {
                    if (_launcherHint) { _launcherHint.textContent = 'Launcher spawned — waiting for backend...'; _launcherHint.className = 'server-offline-hint busy'; }
                    const ready = await _waitForBackendReady(15000);
                    if (ready) {
                        if (_launcherHint) { _launcherHint.textContent = 'Server live — connecting...'; _launcherHint.className = 'server-offline-hint ok'; }
                        connectWebSocket();
                        _pollLauncherStatus();
                    } else if (_launcherHint) {
                        _launcherHint.textContent = 'Launcher gestart maar backend nog niet klaar. Even wachten...';
                        _launcherHint.className = 'server-offline-hint err';
                    }
                } else {
                    if (_launcherHint) { _launcherHint.textContent = fbData.message || 'Spawn failed.'; _launcherHint.className = 'server-offline-hint err'; }
                }
            } catch (e) {
                if (_launcherHint) {
                    _launcherHint.textContent = 'Launcher en backend niet bereikbaar — start handmatig (run start.py).';
                    _launcherHint.className = 'server-offline-hint err';
                }
            }
            if (_launcherStartBtn) _launcherStartBtn.disabled = false;
            if (_launcherRestartBtn) _launcherRestartBtn.disabled = false;
            return;
        }

        // --- Primary: launcher is available ---
        try {
            const r = await fetch(`${LAUNCHER_URL}/api/launcher/${action}`, {
                method: 'POST',
                signal: AbortSignal.timeout(30000),
            });
            const d = await r.json();
            if (d.ok) {
                const launcherMessage = String(d.message || '').trim().toLowerCase();
                if (_launcherHint) {
                    _launcherHint.textContent = launcherMessage === 'already running'
                        ? 'Server already running — reconnecting...'
                        : launcherMessage === 'restarted'
                            ? 'Server restarting — waiting for connection...'
                            : 'Server starting — waiting for connection...';
                    _launcherHint.className = 'server-offline-hint busy';
                }
                const ready = await _waitForBackendReady(action === 'restart' ? 20000 : 12000);
                if (ready) {
                    if (_launcherHint) {
                        _launcherHint.textContent = launcherMessage === 'already running'
                            ? 'Server already running — connecting...'
                            : 'Server live — connecting...';
                        _launcherHint.className = 'server-offline-hint ok';
                    }
                    connectWebSocket();
                } else if (_launcherHint) {
                    _launcherHint.textContent = 'Launcher reageert, maar backend nog niet. Probeer Restart.';
                    _launcherHint.className = 'server-offline-hint err';
                }
                _pollLauncherStatus();
            } else {
                if (_launcherHint) { _launcherHint.textContent = d.message || 'Start failed.'; _launcherHint.className = 'server-offline-hint err'; }
            }
        } catch (e) {
            if (_launcherHint) { _launcherHint.textContent = 'Request failed: ' + e.message; _launcherHint.className = 'server-offline-hint err'; }
        }
        if (_launcherStartBtn) _launcherStartBtn.disabled = false;
        if (_launcherRestartBtn) _launcherRestartBtn.disabled = false;
    }

    if (_launcherStartBtn) _launcherStartBtn.addEventListener('click', () => _launcherAction('start'));
    if (_launcherRestartBtn) _launcherRestartBtn.addEventListener('click', () => _launcherAction('restart'));
    if (_launcherCloseBtn) _launcherCloseBtn.addEventListener('click', _dismissOfflineOverlay);

    // --- WebSocket + auto-reconnect ---
    let _reconnectTimer   = null;
    let _reconnectAttempt = 0;
    const RECONNECT_INTERVALS = [2000, 5000, 10000, 20000]; // ms between attempts

    function _scheduleReconnect() {
        if (_pageClosing) return;
        if (_reconnectTimer) return;                          // already scheduled
        const delay = RECONNECT_INTERVALS[
            Math.min(_reconnectAttempt, RECONNECT_INTERVALS.length - 1)
        ];
        _reconnectAttempt++;
        const secs = Math.round(delay / 1000);
        setStatus(`Disconnected — retrying in ${secs}s… (attempt ${_reconnectAttempt})`, 'error');
        _pollLauncherStatus();
        const launcherState = String(_launcherStatusSnapshot?.state || '');
        if (_reconnectAttempt >= (launcherState === 'starting' ? 4 : 3)) _showOfflineOverlay();
        _reconnectTimer = setTimeout(() => {
            _reconnectTimer = null;
            setStatus('Connecting…', 'running', true);
            connectWebSocket();
        }, delay);
    }

    async function connectWebSocket() {
        if (_pageClosing) return;
        if (ws) {
            try { ws.close(); } catch (_) {}
        }

        if (!_getAuthToken()) {
            try {
                await _refreshAuthToken({ preferDiscovery: true });
            } catch (_) {}
        }

        const resolvedToken = String(_getAuthToken() || '').trim();
        const candidates = _webSocketHosts();

        const markDisconnected = () => {
            if (wsHealthEl) { wsHealthEl.className = 'ws-health disconnected'; wsHealthEl.title = 'Disconnected'; }
            if (startBtn) startBtn.disabled = false;
            if (chatBtn) chatBtn.disabled = false;
            if (resetServerNote) { resetServerNote.textContent = 'Server offline'; resetServerNote.className = 'settings-note err'; }
            for (const [rid, run] of runs.entries()) {
                if (_isRunActive(run)) {
                    run._llmActive = false;
                    run._killRequested = false;
                    run.lane.classList.add('lane-done');
                    _updateRunTabState(rid, 'done');
                }
            }
            _refreshWorkflowIndicator();
        };

        if (!resolvedToken) {
            markDisconnected();
            _scheduleReconnect();
            return;
        }

        const token = encodeURIComponent(resolvedToken);

        const attemptConnect = (index) => {
            const host = candidates[index];
            if (!host) {
                ws = null;
                markDisconnected();
                _scheduleReconnect();
                return;
            }

            const socket = new WebSocket(`${_buildWebSocketUrl(host)}?token=${token}`);
            let opened = false;
            ws = socket;

            socket.onopen = () => {
                opened = true;
                _preferredWsHost = host;
                const wasReconnecting = _reconnectAttempt > 0 || _offlineShown;
                _reconnectAttempt = 0;
                clearTimeout(_reconnectTimer);
                _reconnectTimer = null;
                _hideOfflineOverlay();
                if (wasReconnecting) showToast('success', 'Reconnected to server');
                if (localStorage.getItem('ao_disclaimer_v2')) {
                    if (disclaimerOverlay) disclaimerOverlay.style.display = 'none';
                }
                _notifyLauncherClientState('ws-open');
                if (wsHealthEl) { wsHealthEl.className = 'ws-health connected'; wsHealthEl.title = 'Connected'; }
                if (resetServerNote) { resetServerNote.textContent = '✅ Server live and connected'; resetServerNote.className = 'settings-note ok'; }
                setStatus('Connected to server', 'success');
                loadSessions();
                _scheduleUiUnlockSweep('ws-open');
            };

            socket.onmessage = async (event) => {
                let data;
                try {
                    data = await _parseWebSocketMessage(event);
                } catch (error) {
                    console.warn('Invalid ws payload:', event.data, error);
                    return;
                }
                try {
                    handleMessage(data);
                } catch (error) {
                    console.error('WS message handling failed:', error, data);
                    if (data?.type === 'final_report') {
                        try {
                            _removeThinkingBubble();
                            _finalizeStepBubbles();
                            _buildReportBubbleDOM(data.goal || goalInput.value || '', data.content || '(empty final report)', data.output_files || []);
                            _scrollChatToBottom();
                        } catch (_reportFallbackError) {
                            console.error('Final report fallback render failed:', _reportFallbackError);
                            try {
                                _buildChatBubbleDOM('app', data.content || '(empty final report)', '');
                                _scrollChatToBottom();
                            } catch (_fallbackError) {
                                console.error('Plain final report fallback failed:', _fallbackError);
                            }
                        }
                    }
                }
            };

            socket.onerror = () => {
                if (opened && wsHealthEl) {
                    wsHealthEl.className = 'ws-health disconnected';
                    wsHealthEl.title = 'Disconnected';
                }
            };

            socket.onclose = async (event) => {
                // Token rejected by server — re-fetch and retry immediately
                if (event.code === 4401) {
                    if (ws === socket) ws = null;
                    await _refreshAuthToken({ preferDiscovery: true });
                    if (!_pageClosing) connectWebSocket();
                    return;
                }
                if (!opened && index + 1 < candidates.length) {
                    if (ws === socket) ws = null;
                    attemptConnect(index + 1);
                    return;
                }
                if (ws !== socket) return;
                ws = null;
                if (_pageClosing) return;
                markDisconnected();
                void _notifyLauncherClientState('ws-closed');
                _scheduleReconnect();
            };
        };

        attemptConnect(0);
    }

    // --- Message Handler ---
    function _normalizeWorkflowUiText(text) {
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function _clipWorkflowUiText(text, limit = 140) {
        const normalized = _normalizeWorkflowUiText(text);
        if (!normalized) return '';
        if (normalized.length <= limit) return normalized;
        return normalized.slice(0, Math.max(0, limit - 1)).trimEnd() + '...';
    }

    function _looksTechnicalWorkflowUiText(text) {
        const normalized = _normalizeWorkflowUiText(text).toLowerCase();
        if (!normalized) return false;
        return /http_request|search_workspace|list_files|analyze_attachments|tool call|function call|stream error|forbidden|status code|\/api\/|localhost:|\b403\b|\b404\b|\b500\b|\bjson\b|\bendpoint\b|\bfast-path\b|\bplan b\b|\bllm\b|\bmodel\b|\bollama\b|\btoken\b/.test(normalized);
    }

    function _workflowGoalHint(text) {
        const normalized = _normalizeWorkflowUiText(text).toLowerCase();
        if (!normalized) return 'Werkt aan de volgende stap.';
        if (/\bnaam|\bnamen|name|names/.test(normalized)) return 'Zoekt de gevraagde namen.';
        if (/pdf|bestand|bestanden|file|files|document|documenten|attachment|bijlage|folder|map/.test(normalized)) return 'Controleert relevante bestanden en documenten.';
        if (/email|mail|reply|inbox|thread|bericht/.test(normalized)) return 'Werkt aan de e-mailtaak.';
        if (/calendar|agenda|meeting|event|afspraak/.test(normalized)) return 'Werkt aan de agenda- of meetingtaak.';
        if (/status|ping|service|server|verbinding|connection|bereikbaar/.test(normalized)) return 'Controleert of de benodigde dienst bereikbaar is.';
        if (/research|search|evidence|context|informatie|bron/.test(normalized)) return 'Verzamelt alleen de relevante informatie.';
        if (/user|gebruiker|provide the file|bestand aanleveren|aanvullende input/.test(normalized)) return 'Bepaalt of er nog extra input nodig is.';
        return 'Werkt aan de volgende stap.';
    }

    function _userVisibleStepText(text) {
        const normalized = _normalizeWorkflowUiText(text).replace(/^step\s+\d+(?:\/\d+)?:\s*/i, '');
        if (!normalized) return 'Werkt aan de volgende stap.';
        if (_looksTechnicalWorkflowUiText(normalized)) return _workflowGoalHint(normalized);
        return _clipWorkflowUiText(normalized, 120);
    }

    function _userVisibleIssueText(text) {
        const normalized = _normalizeWorkflowUiText(text).toLowerCase();
        if (!normalized) return '';
        if (/no names|geen namen|requested names|gevraagde namen/.test(normalized)) return 'De gevraagde namen zijn nog niet gevonden.';
        if (/provide the file|request the user|ask user|aanleveren|bestand|extra input/.test(normalized)) return 'Er is aanvullende input nodig om dit af te ronden.';
        if (/wrong endpoint|\/api\/|\b403\b|\b404\b|forbidden|function call failed|stream error|tool|status code|localhost:/.test(normalized)) return 'De gekozen aanpak leverde nog niet het juiste resultaat op.';
        if (/does not fully satisfy|not fully address|does not request|nog niet volledig/.test(normalized)) return 'Het resultaat voldoet nog niet volledig aan de vraag.';
        return 'Het resultaat is nog niet bruikbaar genoeg.';
    }

    function _userVisibleApproachText(text) {
        const normalized = _normalizeWorkflowUiText(text);
        if (!normalized) return '';
        const lowered = normalized.toLowerCase();
        if (lowered.includes('plan b')) return 'Probeert een alternatieve aanpak.';
        if (lowered.includes('retry')) return 'Doet een nieuwe poging.';
        if (lowered.includes('checking') || lowered.includes('validate') || lowered.includes('controle')) return 'Controleert of het resultaat klopt.';
        if (lowered.includes('context') || lowered.includes('curat')) return 'Selecteert de relevante context.';
        if (_looksTechnicalWorkflowUiText(normalized)) return '';
        return _clipWorkflowUiText(normalized, 110);
    }

    function _userVisibleResultPreview(text) {
        const normalized = _normalizeWorkflowUiText(text);
        if (!normalized || _looksTechnicalWorkflowUiText(normalized)) return '';
        return _clipWorkflowUiText(normalized, 120);
    }

    function _userVisibleStatusText(text) {
        const normalized = _normalizeWorkflowUiText(text);
        if (!normalized) return '';
        const lowered = normalized.toLowerCase();
        if (lowered === 'workflow is idle.' || lowered === 'workflow is idle') return '';
        if (lowered.includes('starting plan b')) return 'Probeert een alternatieve aanpak.';
        if (lowered.includes('validating plan b') || lowered.includes('re-validating') || lowered.includes('validating')) return 'Controleert of het resultaat klopt.';
        if (lowered.includes('curating context')) return 'Selecteert de relevante context.';
        if (lowered.includes('executing')) return 'Voert de stap uit.';
        if (lowered.includes('checking if goal is fully achieved')) return 'Controleert of alles is afgerond.';
        if (/^step\s+\d+(?:\/\d+)?:/i.test(normalized)) return _userVisibleStepText(normalized);
        if (_looksTechnicalWorkflowUiText(normalized)) return _workflowGoalHint(normalized);
        return _clipWorkflowUiText(normalized, 120);
    }

    function _userVisibleInfoText(text) {
        const normalized = _normalizeWorkflowUiText(text);
        if (!normalized) return '';
        const lowered = normalized.toLowerCase();
        if (lowered.includes('fallback model') || lowered.includes('primary model failed') || lowered.includes('fast model failed')) {
            return 'De workflow schakelt over zodat het verzoek verder kan worden afgehandeld.';
        }
        if (_looksTechnicalWorkflowUiText(normalized)) return '';
        return _clipWorkflowUiText(normalized, 120);
    }

    function _userVisibleAgentStatusText(agent, state, detailText = '') {
        const normalizedAgent = String(agent || '').trim().toLowerCase();
        const normalizedState = String(state || '').trim().toLowerCase();
        const detail = _normalizeWorkflowUiText(detailText);
        if (normalizedAgent === 'all' || normalizedState === 'idle' || normalizedState === 'skip') return '';
        if (normalizedAgent === 'validator' && /^pass\b/i.test(detail)) return '';
        if (normalizedState === 'error' || (normalizedAgent === 'validator' && /^issue\b/i.test(detail))) {
            return _userVisibleIssueText(detail);
        }
        const byAgent = {
            planner: {
                active: 'Bepaalt de aanpak.',
                done: 'De aanpak staat klaar.',
            },
            researcher: {
                active: 'Zoekt relevante informatie.',
                done: 'Relevante informatie is verzameld.',
            },
            curator: {
                active: 'Selecteert de relevante context.',
                done: 'De relevante context is geselecteerd.',
            },
            executor: {
                active: _workflowGoalHint(detail),
                done: 'De stap is uitgevoerd.',
            },
            validator: {
                active: 'Controleert of het resultaat klopt.',
                done: '',
            },
            reportout: {
                active: 'Maakt het antwoord gereed.',
                done: 'Het antwoord is gereed om te delen.',
            },
        };
        const mapping = byAgent[normalizedAgent] || {};
        const fallback = normalizedState === 'done' ? 'Dit onderdeel is afgerond.' : _workflowGoalHint(detail);
        return mapping[normalizedState] || fallback;
    }

    function _userVisibleHandoffText(fromAgent, toAgent) {
        const fromLabel = String(fromAgent || 'Agent').replace(/^./, (value) => value.toUpperCase());
        const toLabel = String(toAgent || 'Agent').replace(/^./, (value) => value.toUpperCase());
        const key = `${String(fromAgent || '').toLowerCase()}->${String(toAgent || '').toLowerCase()}`;
        const mapping = {
            'planner->researcher': 'De aanpak is bepaald en er wordt nu informatie verzameld.',
            'researcher->curator': 'De gevonden informatie wordt teruggebracht tot wat echt relevant is.',
            'curator->executor': 'De geselecteerde context wordt gebruikt om de stap uit te voeren.',
            'executor->validator': 'Het resultaat wordt gecontroleerd op bruikbaarheid.',
            'validator->executor': 'Er volgt een nieuwe poging om het resultaat te verbeteren.',
            'validator->reportout': 'De uitkomst wordt omgezet naar een duidelijk antwoord.',
        };
        return mapping[key] || `${fromLabel} draagt relevante informatie over aan ${toLabel}.`;
    }

    function _sanitizeTodoProgressData(data) {
        const items = Array.isArray(data?.items)
            ? data.items.map((item) => ({
                ...item,
                step: _userVisibleStepText(item?.step || ''),
                approach: _userVisibleApproachText(item?.approach || ''),
                evaluation: _userVisibleIssueText(item?.evaluation || ''),
                result_preview: _userVisibleResultPreview(item?.result_preview || ''),
            }))
            : [];
        return { ...data, items };
    }

    function handleMessage(data) {
        const incomingRunId = data.run_id || '';
        if (incomingRunId && _discardedRunIds.has(incomingRunId) && data.type !== 'run_start') {
            return;
        }
        if (incomingRunId) _currentHandlingRunId = incomingRunId;
        switch (data.type) {
            case 'run_start':
                if (incomingRunId) _discardedRunIds.delete(incomingRunId);
                _createRunLane(data.run_id, data.goal || data.run_id);
                if (incomingRunId && runs.has(incomingRunId) && _pendingRunSessionIds.has(incomingRunId)) {
                    const _pendingSessionId = _pendingRunSessionIds.get(incomingRunId) || '';
                    if (_pendingSessionId) {
                        _bindConversationSession(_pendingSessionId, incomingRunId, activeRunId === incomingRunId);
                    }
                    _pendingRunSessionIds.delete(incomingRunId);
                }
                if (incomingRunId && runs.has(incomingRunId) && !_sessionIdForRun(incomingRunId)) {
                    const _queuedSessionId = _takeAnonymousSessionId();
                    if (_queuedSessionId) {
                        _bindConversationSession(_queuedSessionId, incomingRunId, activeRunId === incomingRunId || !activeRunId);
                    }
                }
                if (incomingRunId && runs.has(incomingRunId) && _pendingPreRunChatBubbles.length) {
                    const _pendingRun = runs.get(incomingRunId);
                    _pendingRun._chatBubbles.unshift(..._pendingPreRunChatBubbles);
                    _pendingPreRunChatBubbles = [];
                    if (activeRunId === incomingRunId) {
                        _renderChatForActiveRun();
                    }
                }
                // _createRunLane calls _setActiveRun which sets activeRunId
                _appendChatBubble('system', `Workflow started — agents are working on your request.`);
                _refreshWorkflowIndicator();
                break;

            case 'paused': {
                const _pausedRun = runs.get(data.run_id || _currentHandlingRunId);
                if (_pausedRun) {
                    _pausedRun.lane.classList.add('lane-paused');
    
                }
                setStatus('Paused', 'idle');
                break;
            }

            case 'resumed': {
                const _resumedRun = runs.get(data.run_id || _currentHandlingRunId);
                if (_resumedRun) {
                    _resumedRun.lane.classList.remove('lane-paused');

                }
                setStatus('Running\u2026', 'running', true);
                break;
            }

            case 'run_done': {
                const _rdRun = runs.get(data.run_id);
                if (_rdRun) {
                    _rdRun.lane.classList.add('lane-done');
                    _rdRun._llmActive = false;
                    _rdRun._killRequested = false;
                }
                _updateRunTabState(data.run_id, 'done');
                _appendChatBubble('system', `Workflow completed.`);
                _refreshWorkflowIndicator();
                loadSessions();
                break;
            }

            case 'run_stopped': {
                appendLog(data.content || 'Workflow stopped.', 'error');
                const reason = String(data.reason_code || '').toUpperCase();
                const remove = reason === 'USER_STOPPED' || reason === 'ALREADY_STOPPED';
                _finalizeRunStopped(data.run_id || _currentHandlingRunId, true, remove);
                if (reason === 'USER_STOPPED') {
                    setStatus('Workflow kill confirmed.', 'idle');
                }
                break;
            }

            case 'run_stop_requested': {
                const rid = data.run_id || _currentHandlingRunId;
                if (rid && runs.has(rid)) {
                    const _rqRun = runs.get(rid);
                    _rqRun._killRequested = true;
                    _rqRun._killRequestedAt = Date.now();
                }
                appendLog(data.content || 'Kill requested. Waiting for current model call to finish...', 'info');
                setStatus('Kill requested - waiting for current model call to finish...', 'running', true);
                _renderActiveWorkflowList();
                break;
            }

            case 'session_id':
                if (incomingRunId) {
                    const _shouldPromoteSession = incomingRunId === activeRunId || (!activeRunId && runs.size === 0);
                    _bindConversationSession(data.session_id, incomingRunId, _shouldPromoteSession);
                } else {
                    const _bindableRunId = _findBindableLiveRunId();
                    if (_bindableRunId) {
                        _bindConversationSession(data.session_id, _bindableRunId, _bindableRunId === activeRunId || (!activeRunId && runs.size === 0));
                    } else if (data.session_id) {
                        _enqueueAnonymousSessionId(data.session_id);
                        _bindConversationSession(data.session_id, '', true);
                    }
                }
                // Only update the global session + pill if this is the active run,
                // or there is no active lane yet for a freshly-started conversation.
                break;

            case 'chat_reply': {
                _removeThinkingBubble();
                const _replyText = data.content || '(no reply)';
                const _chatRid = _currentHandlingRunId || activeRunId;
                const _hasRunLane = _chatRid && runs.has(_chatRid);
                // If we're waiting for a draft reply inside the email popup, route it there
                if (_draftReplyPending && _emailReaderModal && _draftReplyRequest?.transport === 'ws') {
                    if (_isTransientDraftModelIssue(_replyText) && _draftReplyRetryCount < _maxDraftReplyRetryCount()) {
                        _draftReplyRetryCount += 1;
                        const retried = _sendDraftReplyRequest();
                        if (retried) {
                            showToast('warning', 'Model temporarily unavailable. Retrying with a fallback model…', 4500);
                            setStatus('Retrying draft generation…', 'running', true);
                            break;
                        }
                    }
                    if (_isTransientDraftModelIssue(_replyText)) {
                        _draftReplyPending = false;
                        _showDraftReplyRetryState(_replyText || 'LLM not available.');
                        setStatus('Draft generation failed', 'error');
                        break;
                    }
                    _resetDraftReplyState();
                    _resolveDraftReplyInPopup(_replyText);
                    break;
                }
                if (_consumePendingInlineEmailReply(_replyText)) {
                    appendLog('Email content loaded into popup.', 'success', 'Chat', true);
                    break;
                }
                // If an inline email reader is waiting, route the reply into it (skip normal render)
                if (_resolveEmailInlineReply(_replyText)) {
                    appendLog(_replyText, 'success', 'Chat', true /* skipMirror */);
                } else {
                    appendLog(_replyText, 'success', 'Chat', !_hasRunLane);
                    // Show reply as chat bubble — force render when no run lane exists
                    if (!_hasRunLane) {
                        _buildChatBubbleDOM('app', _replyText, '');
                        _scrollChatToBottom();
                    }
                }
                if (data.session_id) {
                    // Direct chat responses can arrive without a lane; keep the
                    // session globally active so later follow-ups stay in one chat.
                    _bindConversationSession(
                        data.session_id,
                        _chatRid,
                        !_hasRunLane || !incomingRunId || incomingRunId === activeRunId,
                    );
                }
                setStatus('Connected to server', 'success');
                if (startBtn) startBtn.disabled = false;
                if (chatBtn) chatBtn.disabled = false;
                break;
            }

            /* ── Auto-escalation: backend automatically starts a workflow ── */
            case 'chat_escalate': {
                const _escRid = incomingRunId || _currentHandlingRunId || activeRunId;
                const _escRun = _escRid ? runs.get(_escRid) : null;
                if (_escRun) {
                    _escRun.lane.classList.remove('lane-done');
                    _escRun._llmActive = true;
                    if (data.goal) {
                        const _escGoalEl = _escRun.lane.querySelector('.lane-goal');
                        if (_escGoalEl) { _escGoalEl.textContent = data.goal; _escGoalEl.title = data.goal; }
                    }
                    _updateRunTabState(_escRid, 'running');
                }
                // Suppress workflow bubble when email modal is open
                if (!_emailReaderModal) {
                    _appendChatBubble('system', 'Escalating to full workflow — agents are working on your request.');
                }
                _refreshWorkflowIndicator();
                setStatus('Running…', 'running', true);
                break;
            }

            /* ── Legacy: suggest agent (kept for backward compat) ── */
            case 'chat_suggest_agent': {
                const suggestGoal = data.goal || '';
                if (suggestGoal && chatThread) {
                    _showAgentSuggestBubble(suggestGoal);
                }
                break;
            }

            case 'chat_reply_offer_agent': {
                /* The LLM itself offered to start an agent flow */
                const offerGoal = data.goal || '';
                if (offerGoal && chatThread) {
                    _showAgentSuggestBubble(offerGoal, data.text || '');
                }
                break;
            }

            case 'memory_recall':
                if (data.count > 0) {
                    recalledMemoryRaw = data.content || '';
                    appendMemoryRecallLog(recalledMemoryRaw, data.count);
                } else {
                    recalledMemoryRaw = '';
                    appendLog('No past sessions found — starting fresh.', 'info');
                }
                break;

            case 'compact_result': {
                _removeThinkingBubble();
                const _compactMsg = `Session compacted: ${data.before} \u2192 ${data.after} messages. Summary preserved.`;
                _appendChatBubble('system', _compactMsg);
                appendLog(_compactMsg, 'info');
                setStatus('Connected to server', 'success');
                break;
            }

            case 'compact_suggest': {
                const _suggestMsg = `\uD83D\uDCA1 Session getting long (${data.message_count} messages) \u2014 type /compact to optimize context`;
                _appendChatBubble('system', _suggestMsg);
                break;
            }

            case 'context_curated':
                if (data.briefing) {
                    appendLog('Relevante context is geselecteerd voor deze stap.', 'curated', 'Curator');
                } else {
                    appendLog('Deze stap kon zonder extra context worden uitgevoerd.', 'curated', 'Curator');
                }
                break;

            case 'research_ready':
                appendLog('Relevante informatie is verzameld voor deze stap.', 'research', 'Researcher');
                break;

            case 'info': {
                const _infoText = _userVisibleInfoText(data.content);
                if (_infoText) appendLog(_infoText, 'info');
                if (typeof data.content === 'string') {
                    const msg = data.content.trim().toLowerCase();
                    if (msg === 'orchestration stopped by user.' || msg.startsWith('run stopped:')) {
                        _finalizeRunStopped(data.run_id || _currentHandlingRunId, msg.startsWith('run stopped:'), true);
                    }
                }
                break;
            }

            case 'status': {
                setStatus(data.content, 'running', true);
                const _statusText = _userVisibleStatusText(data.content);
                if (_statusText && _statusText !== _lastStatusMessage) {
                    _lastStatusMessage = _statusText;
                    appendLog(_statusText, 'info', 'Status');
                    _appendProcessThinkingLine(_statusText, incomingRunId);
                }
                break;
            }

            case 'step_start': {
                setStatus(data.content, 'running', true);
                const _ssRun = runs.get(_currentHandlingRunId);
                const _ssTot = _ssRun ? _ssRun.totalSteps : totalSteps;
                const _stepText = _userVisibleStepText(data.content);
                if (_ssRun) {
                    _ssRun._stepsDone = Math.max(_ssRun._stepsDone || 0, (Number(data.step_index) || 0) + 1);
                    _ssRun._activePlanStepIndex = Number(data.step_index) || 0;
                    if (_currentHandlingRunId === activeRunId) _renderBudgetCounter(_currentHandlingRunId);
                }
                updateProgressBar(data.step_index, _ssTot);
                // Highlight active plan step
                if (_ssRun && _ssRun.planList) {
                    _ssRun.planList.querySelectorAll('li').forEach((li, idx) => {
                        if (idx < data.step_index) {
                            if (li.dataset.state !== 'issue') _setPlanStepState(li, 'completed');
                        } else if (idx === data.step_index) {
                            _setPlanStepState(li, 'active');
                        } else if (li.dataset.state !== 'issue') {
                            _setPlanStepState(li, 'pending');
                        }
                    });
                }
                // Show step in chat
                _appendStepBubble(data.step_index, _ssTot, _stepText);
                _appendProcessThinkingLine(_stepText || `Stap ${Number(data.step_index || 0) + 1}: bezig`, incomingRunId);
                break;
            }

            case 'error': {
                if (_draftReplyPending && _emailReaderModal && _draftReplyRequest?.transport === 'ws') {
                    const errText = String(data.content || '').trim();
                    const transientModelIssue = _isTransientDraftModelIssue(errText);
                    if (transientModelIssue && _draftReplyRetryCount < _maxDraftReplyRetryCount()) {
                        _draftReplyRetryCount += 1;
                        const retried = _sendDraftReplyRequest();
                        if (retried) {
                            showToast('warning', 'Model temporarily unavailable. Retrying with a fallback model…', 4500);
                            setStatus('Retrying draft generation…', 'running', true);
                            break;
                        }
                    }
                    _draftReplyPending = false;
                    _showDraftReplyRetryState(errText || 'Could not generate a reply draft.');
                    setStatus('Draft generation failed', 'error');
                    showToast('error', errText || 'Could not generate a reply draft.', 6000);
                    break;
                }
                _removeThinkingBubble();
                _finalizeStepBubbles();
                appendLog(data.content, 'error');
                setStatus('Error', 'error');
                if (startBtn) startBtn.disabled = false;
                if (chatBtn) chatBtn.disabled = false;
                const _errRun = runs.get(_currentHandlingRunId);
                if (_errRun) {
                    const activeStep = _getPlanStepAt(_errRun.planList, Number(_errRun._activePlanStepIndex));
                    if (activeStep && activeStep.dataset.state !== 'completed') {
                        _setPlanStepState(activeStep, 'issue');
                    }
                    _errRun.lane.classList.add('lane-error');
                    _errRun._llmActive = false;
                    _errRun._killRequested = false;

                }
                _updateRunTabState(_currentHandlingRunId, 'error');
        setStatus('Generating draft…', 'running', true);
                break;
            }

            case 'plan': {
                const planSteps = Array.isArray(data.content) ? data.content : [];
                const _userPlanSteps = planSteps.map((step) => _userVisibleStepText(step));
                totalSteps = planSteps.length;
                updateProgressBar(0, totalSteps);
                const _planRun = runs.get(_currentHandlingRunId);
                if (_planRun) {
                    _planRun.totalSteps = totalSteps;
                    _planRun._activePlanStepIndex = -1;
                    _planRun.planList.innerHTML = '';
                    _userPlanSteps.forEach(s => {
                        _planRun.planList.appendChild(_createPlanStepItem(s));
                    });
                    _planRun.planEl.style.display = 'block';
                }
                appendLog(`Aanpak klaar - ${totalSteps} stap${totalSteps !== 1 ? 'pen' : ''}.`, 'success', 'Planner');
                // Show plan in chat
                _appendPlanBubble(_userPlanSteps);
                break;
            }

            case 'todo_progress': {
                _applyTodoProgress(incomingRunId, _sanitizeTodoProgressData(data));
                break;
            }

            case 'step_result': {
                const verdict = data.verdict || '';
                const isIssue = verdict.toUpperCase().startsWith('ISSUE');
                const icon = isIssue ? '⚠️' : '✓';
                appendLog(`${icon} Stap afgerond: ${_userVisibleStepText(data.step)}`, isIssue ? 'error' : 'success', 'Executor');
                if (isIssue) {
                    appendLog(_userVisibleIssueText(verdict), 'error', 'Validator');
                }
                const _srRun = runs.get(_currentHandlingRunId);
                if (_srRun) {
                    const activeStep = _getPlanStepAt(_srRun.planList, Number(_srRun._activePlanStepIndex));
                    if (activeStep) _setPlanStepState(activeStep, isIssue ? 'issue' : 'completed');
                }
                updateMemory(data.memory);
                break;
            }

            case 'step_draft': {
                /* Step-draft is an intermediate executor result.
                   If a final_report follows, showReportModal() removes these
                   to avoid showing the answer twice. */
                appendLog(`📝 Live output received for step.`, 'info', 'Executor');
                break;
            }

            case 'success': {
                _removeThinkingBubble();
                _finalizeStepBubbles();
                setStatus(data.content, 'success');
                _updateModelLabel();
                const _succRun = runs.get(_currentHandlingRunId);
                const _succTot = _succRun ? _succRun.totalSteps : totalSteps;
                updateProgressBar(_succTot, _succTot);
                if (startBtn) startBtn.disabled = false;
                appendLog(data.content, 'success');
                // Suppress "done" bubble when email modal is open
                if (!_emailReaderModal) {
                    _appendChatBubble('system', `✓ ${data.content || 'Workflow completed.'}`);
                }
                loadSessions();
                if (_succRun) {
                    _succRun.planList && _succRun.planList.querySelectorAll('li').forEach(li => {
                        if (li.dataset.state !== 'issue') _setPlanStepState(li, 'completed');
                    });
                    _succRun._activePlanStepIndex = -1;
                    _succRun.lane.classList.add('lane-done');
                    _succRun._llmActive = false;
                    _succRun._killRequested = false;

                }
                _refreshWorkflowIndicator();
                break;
            }

            case 'final_report': {
                _removeThinkingBubble();
                _finalizeStepBubbles();
                if (_consumePendingInlineEmailReply(data.content || '')) {
                    appendLog('Email content loaded into popup.', 'success', null, true);
                    const _frRunE = runs.get(_currentHandlingRunId);
                    if (_frRunE) {
                        _frRunE.lane.classList.add('lane-done');
                        _frRunE._llmActive = false;
                        _frRunE._killRequested = false;
                        _updateRunTabState(_currentHandlingRunId, 'done');
                        _refreshWorkflowIndicator();
                    }
                    break;
                }
                appendLog('Final report generated and opened.', 'success');
                const _frRun = runs.get(_currentHandlingRunId);
                if (_frRun) {
                    _frRun._reportContent = data.content || '';
                    _frRun._reportGoal = data.goal || goalInput.value || '';
                    _frRun._outputFiles = data.output_files || [];
                    if (activeRunId !== _currentHandlingRunId) {
                        _setActiveRun(_currentHandlingRunId);
                    }
                    if (!_frRun.lane.classList.contains('lane-done')) {
                        _frRun.lane.classList.add('lane-done');
                        _frRun._llmActive = false;
                        _frRun._killRequested = false;
                        _updateRunTabState(_currentHandlingRunId, 'done');
                        _refreshWorkflowIndicator();
                    }
                }
                _appendReportBubble(data.goal || goalInput.value || '', data.content || '', data.output_files || []);
                break;
            }

            case 'stream_start': {
                const _sPane = _getRunContainer(data.run_id);
                const _sEntry = document.createElement('div');
                _sEntry.className = 'log-entry stream stream-active';
                const stag = document.createElement('span');
                stag.className = 'log-tag';
                stag.textContent = 'OUT';
                const _sBody = document.createElement('span');
                _sBody.className = 'log-body stream-body';
                _sEntry.appendChild(_logTimestamp());
                const _sagent = document.createElement('span');
                _sagent.className = 'log-agent';
                _sagent.textContent = data.agent || 'Executor';
                _sEntry.appendChild(_sagent);
                _sEntry.appendChild(stag);
                _sEntry.appendChild(_sBody);
                _sPane.appendChild(_sEntry);
                _sPane.scrollTop = _sPane.scrollHeight;
                if (data.run_id && runs.has(data.run_id)) {
                    const _sr = runs.get(data.run_id);
                    _sr._streamEntry = _sEntry; _sr._streamBody = _sBody;
                } else { _streamEntry = _sEntry; _streamBody = _sBody; }
                break;
            }

            case 'stream_token': {
                const _stRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                if (_stRun) {
                    if (_stRun._streamBody) _stRun._streamBody.textContent += data.content;
                    _stRun.logPane.scrollTop = _stRun.logPane.scrollHeight;
                } else {
                    if (_streamBody) _streamBody.textContent += data.content;
                }
                break;
            }

            case 'stream_end': {
                const _seRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                if (_seRun) {
                    if (_seRun._streamEntry) _seRun._streamEntry.classList.remove('stream-active');
                    _seRun._lastStreamEntry = _seRun._streamEntry; // keep ref for stream_clear
                    _seRun._streamEntry = null; _seRun._streamBody = null;
                } else {
                    if (_streamEntry) _streamEntry.classList.remove('stream-active');
                    _lastStreamEntry = _streamEntry; // keep ref for stream_clear
                    _streamEntry = null; _streamBody = null;
                }
                break;
            }

            case 'stream_clear': {
                // Hide the last streamed output block (it contained a tool call, not user content)
                const _clRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                const _target = _clRun ? _clRun._lastStreamEntry : _lastStreamEntry;
                if (_target) _target.style.display = 'none';
                break;
            }

            case 'stream_thinking_start': {
                // Insert a thinking block — either inside the current stream entry or as standalone bubble
                const _tkRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                const _tkEntry = _tkRun ? _tkRun._streamEntry : _streamEntry;
                _removeThinkingBubble();
                const details = document.createElement('details');
                details.className = 'thinking-block';
                details.open = _shouldKeepThinkingExpanded();
                details.style.display = 'none';
                const summary = document.createElement('summary');
                summary.textContent = 'Thinking…';
                const body = document.createElement('div');
                body.className = 'thinking-body';
                details.appendChild(summary);
                details.appendChild(body);
                if (_tkEntry) {
                    // Inside a streaming log entry
                    const _sBody = _tkEntry.querySelector('.stream-body');
                    if (_sBody) { _tkEntry.insertBefore(details, _sBody); }
                    else { _tkEntry.appendChild(details); }
                } else {
                    // Chat reply path — show as a standalone thinking bubble in the chat area
                    if (chatThread) {
                        const thinking = _ensureActiveThinkingDetails('Thinking…', _shouldKeepThinkingExpanded());
                        thinking.body.textContent = '';
                        thinking.details.style.display = 'none';
                        details.remove();
                        _tkRun ? (_tkRun._thinkingBody = thinking.body, _tkRun._thinkingDetails = thinking.details) : (_thinkingBody = thinking.body, _thinkingDetails = thinking.details);
                        _forceStickChatBottom(4500);
                        _scrollChatToBottom();
                        break;
                    } else {
                        // Last resort: append to log pane
                        const _tkPane = _getRunContainer(data.run_id);
                        _tkPane.appendChild(details);
                    }
                }
                if (_tkRun) { _tkRun._thinkingBody = body; _tkRun._thinkingDetails = details; }
                else { _thinkingBody = body; _thinkingDetails = details; }
                break;
            }

            case 'stream_thinking': {
                const _thRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                const _tb = _thRun ? _thRun._thinkingBody : _thinkingBody;
                if (_tb) {
                    const chunk = String(data.content || '');
                    // Ignore whitespace-only thinking chunks; these create huge empty gaps.
                    if (!chunk.trim()) break;
                    const existing = String(_tb.textContent || '');
                    _tb.textContent = _normalizeThinkingText(existing + chunk, true, false);
                    const details = _tb.closest('.thinking-block');
                    if (details && _tb.textContent.trim()) {
                        details.style.display = '';
                        const wrapper = details.closest('.thinking-bubble');
                        if (wrapper && wrapper.dataset.compactHidden === '1') {
                            delete wrapper.dataset.compactHidden;
                            wrapper.style.display = '';
                            wrapper.style.height = '';
                            wrapper.style.margin = '';
                            wrapper.style.padding = '';
                        }
                    }
                    _ensureChatThreadVisibleState();
                    _compactChatThreadLayout();
                    // Auto-scroll the parent pane
                    const _thPane = _getRunContainer(data.run_id);
                    _thPane.scrollTop = _thPane.scrollHeight;
                }
                break;
            }

            case 'stream_thinking_end': {
                const _teRun = data.run_id && runs.has(data.run_id) ? runs.get(data.run_id) : null;
                const _td = _teRun ? _teRun._thinkingDetails : _thinkingDetails;
                if (_td) {
                    const body = _td.querySelector('.thinking-body');
                    let normalized = '';
                    if (body) {
                        normalized = _normalizeThinkingText(body.textContent, true, true);
                        body.textContent = normalized;
                    }
                    const s = _td.querySelector('summary');
                    if (s) s.textContent = 'Thought process';
                    // If thinking ended up empty/whitespace, remove the whole block to avoid empty gaps.
                    if (!normalized.trim()) {
                        const wrapper = _td.closest('.thinking-bubble');
                        if (wrapper) wrapper.remove();
                        else _td.remove();
                    } else {
                        const wrapper = _td.closest('.thinking-bubble');
                        if (wrapper) {
                            if (wrapper.dataset.compactHidden === '1') {
                                delete wrapper.dataset.compactHidden;
                                wrapper.style.display = '';
                                wrapper.style.height = '';
                                wrapper.style.margin = '';
                                wrapper.style.padding = '';
                            }
                            wrapper.removeAttribute('id');
                        }
                    }
                    // Respect user preference: keep expanded when Show Thinking is enabled.
                    _td.open = _shouldKeepThinkingExpanded();
                    _setThinkingActivityState(_td, false);
                    _ensureChatThreadVisibleState();
                    _compactChatThreadLayout();
                }
                if (_teRun) { _teRun._thinkingBody = null; _teRun._thinkingDetails = null; }
                else { _thinkingBody = null; _thinkingDetails = null; }
                break;
            }

            case 'retry':
                appendLog('Er wordt een nieuwe poging gedaan om het resultaat te verbeteren.', 'info', 'Validator');
                break;

            case 'plan_extended': {
                const _peRun = runs.get(_currentHandlingRunId);
                if (_peRun) _peRun.totalSteps = data.total;
                totalSteps = data.total;
                appendLog(`Re-planning: ${data.new_steps.length} additional step(s) added to close gaps.`, 'info', 'Planner');
                // Append new steps to the lane plan list
                if (_peRun && _peRun.planList && Array.isArray(data.new_steps)) {
                    data.new_steps.forEach(s => {
                        _peRun.planList.appendChild(_createPlanStepItem(s));
                    });
                    _peRun.planEl.style.display = 'block';
                }
                break;
            }

            case 'agent_status':
                {
                    const _agentRunId = incomingRunId || _currentHandlingRunId || activeRunId;
                    const _agentName = String(data.agent || 'agent');
                    const _agentLabel = _agentName === 'reportout'
                        ? 'Report Out'
                        : (_agentName ? (_agentName.charAt(0).toUpperCase() + _agentName.slice(1)) : 'Agent');
                    const _agentNarration = String(data.narration || '').trim();
                    if (_agentRunId && runs.has(_agentRunId)) {
                        const _asRun = runs.get(_agentRunId);
                        if (_asRun) {
                            const _a = String(data.agent || '');
                            const _label = _a ? (_a === 'reportout' ? 'Report Out' : (_a.charAt(0).toUpperCase() + _a.slice(1))) : '';
                            _asRun._lastAgent = _a;
                            _asRun._lastAgentLabel = _label;
                            _asRun._lastAgentState = String(data.state || '');
                        }
                    }
                    const _visibleAgentText = _userVisibleAgentStatusText(data.agent, data.state, _agentNarration || data.msg || '');
                    setAgentState(data.agent, data.state, _visibleAgentText, _agentRunId);
                    if (_visibleAgentText) {
                        appendLog(_visibleAgentText, 'info', _agentLabel);
                    }
                    if (data.agent && data.agent !== 'all') {
                        _appendAgentActivityBubble(data.agent, data.state, _visibleAgentText);
                    }
                }
                _renderActiveWorkflowList();
                break;

            case 'model_active': {
                if (_currentHandlingRunId && runs.has(_currentHandlingRunId)) {
                    const _maRun = runs.get(_currentHandlingRunId);
                    _maRun._llmActive = true;
                    _maRun._llmLastActiveAt = Date.now();
                }
                if (modelLabelEl && data.model) {
                    const display = data.model.replace(/^(nvidia\/|openai\/|gemini\/|ollama\/)/, '');
                    modelLabelEl.textContent = display;
                    modelLabelEl.title = `Active model: ${data.model}`;
                }
                _refreshWorkflowIndicator();
                break;
            }

            case 'model_idle': {
                if (_currentHandlingRunId && runs.has(_currentHandlingRunId)) {
                    const _miRun = runs.get(_currentHandlingRunId);
                    _miRun._llmActive = false;
                }
                _refreshWorkflowIndicator();
                break;
            }

            case 'agent_handoff': {
                flashConnector(data.from, data.to, incomingRunId);
                const _handoffText = _userVisibleHandoffText(data.from, data.to);
                if (_handoffText) {
                    appendLog(`↪ ${_handoffText}`, 'info', 'Workflow');
                }
                // Show thinking animation between agents
                _showThinkingBubble();
                    _ensureChatThreadVisibleState();
                    _compactChatThreadLayout();
                    _scrollChatToBottom();
                break;
            }

            case 'token_usage':
                if (_currentHandlingRunId && runs.has(_currentHandlingRunId)) {
                    const _tuRun = runs.get(_currentHandlingRunId);
                    if (_tuRun) {
                        if (typeof data.run_total === 'number') {
                            _tuRun._tokenTotal = data.run_total;
                        } else {
                            _tuRun._tokenTotal = (_tuRun._tokenTotal || 0) + (Number(data.tokens) || 0);
                        }
                        if (_currentHandlingRunId === activeRunId) _renderBudgetCounter(_currentHandlingRunId);
                    }
                }
                break;

            case 'tool_call':
                if (_currentHandlingRunId && runs.has(_currentHandlingRunId)) {
                    const _tcRun = runs.get(_currentHandlingRunId);
                    if (_tcRun) {
                        _tcRun._toolsDone = (_tcRun._toolsDone || 0) + 1;
                        if (_currentHandlingRunId === activeRunId) _renderBudgetCounter(_currentHandlingRunId);
                    }
                }
                appendLog(`🔧 Skill call: ${data.tool}(${JSON.stringify(data.args || {})})`, 'tool', 'Executor');
                if (typeof data.tool === 'string' && data.tool.startsWith('browser_')) refreshBrowserPreview(true);
                break;

            case 'tool_result':
                appendLog(`↩ Skill result [${data.tool}]: ${(data.result || '').slice(0, 200)}${(data.result || '').length > 200 ? '…' : ''}`, 'tool', 'Executor');
                if (typeof data.tool === 'string' && data.tool.startsWith('browser_')) refreshBrowserPreview(true);
                break;

            case 'browser_update':
                _handleBrowserUpdate(data);
                break;

            case 'approval_needed':
                showApprovalModal(data.step_index, data.step, data.run_id || _currentHandlingRunId || activeRunId || '');
                appendLog(`⏸ Waiting for approval: Step ${data.step_index + 1} — ${data.step}`, 'hitl');
                break;

            case 'clarifying_question':
                showClarifyPrompt(data.question);
                break;
        }
    }

    let _mcpStatus = {}; // { serverName: { ok, error, tools:[] } }

    function _mcpGetServersObj() {
        try { return JSON.parse(mcpServersInput?.value || '{}'); } catch { return {}; }
    }
    function _mcpSetServersObj(obj) {
        if (mcpServersInput) mcpServersInput.value = JSON.stringify(obj, null, 2);
    }

    function _mcpAddServer(name, cfg) {
        const servers = _mcpGetServersObj();
        if (servers[name]) return false;
        servers[name] = {};
        if (cfg.command) servers[name].command = cfg.command;
        if (cfg.args) servers[name].args = cfg.args;
        if (cfg.env) servers[name].env = { ...cfg.env };
        _mcpSetServersObj(servers);
        _mcpRenderServerList();
        _mcpUpdatePresetChips();
        return true;
    }

    function _mcpRemoveServer(name) {
        const servers = _mcpGetServersObj();
        if (!servers[name]) return;
        delete servers[name];
        _mcpSetServersObj(servers);
        delete _mcpStatus[name];
        _mcpRenderServerList();
        _mcpUpdatePresetChips();
    }

    function _mcpUpsertServer(name, cfg) {
        const servers = _mcpGetServersObj();
        servers[name] = {};
        if (cfg.command) servers[name].command = cfg.command;
        if (cfg.args) servers[name].args = cfg.args;
        if (cfg.env) servers[name].env = { ...cfg.env };
        _mcpSetServersObj(servers);
        _mcpRenderServerList();
        _mcpUpdatePresetChips();
    }

    async function _mcpCallTool(server, tool, argumentsObj) {
        const resp = await authFetch('/api/mcp/call', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ server, tool, arguments: argumentsObj || {} }),
        });
        const data = await resp.json();
        if (!resp.ok || !data.ok) {
            throw new Error((data && data.error) || `MCP call failed (${resp.status})`);
        }
        return data;
    }

    async function _mcpPersistServersOnly() {
        const settingsResp = await authFetch('/api/settings');
        const settingsData = await settingsResp.json();
        const nextSettings = { ...(settingsData || {}) };
        nextSettings.mcp_servers = _mcpGetServersObj();
        const saveResp = await authFetch('/api/settings', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(nextSettings),
        });
        if (!saveResp.ok) {
            throw new Error('Could not save MCP server settings.');
        }
        showToast('success', 'MCP settings saved', 2000);
    }

    function _mcpUpdatePresetChips() {
        if (!mcpPresetGallery) return;
        const servers = _mcpGetServersObj();
        mcpPresetGallery.querySelectorAll('.mcp-preset-chip').forEach(chip => {
            const id = chip.dataset.preset;
            chip.classList.toggle('added', !!servers[id]);
        });
    }

    function _mcpRenderServerList() {
        if (!mcpServerList) return;
        const servers = _mcpGetServersObj();
        const names = Object.keys(servers);
        if (mcpServerCount) mcpServerCount.textContent = names.length || '';
        if (!names.length) { mcpServerList.innerHTML = ''; return; }
        mcpServerList.innerHTML = '';
        for (const name of names) {
            const cfg = servers[name];
            const st = _mcpStatus[name];
            const card = document.createElement('div');
            card.className = 'mcp-server-card';
            const dot = document.createElement('span');
            dot.className = 'mcp-server-status' + (st ? (st.ok ? ' ok' : ' error') : ' pending');
            dot.title = st ? (st.ok ? `Connected — ${(st.tools||[]).length} tools` : `Error: ${st.error||'unknown'}`) : 'Not yet checked';
            const info = document.createElement('div');
            info.className = 'mcp-server-info';
            const nameEl = document.createElement('div');
            nameEl.className = 'mcp-server-name';
            nameEl.textContent = name;
            const meta = document.createElement('div');
            meta.className = 'mcp-server-meta';
            // Always show config line
            const cfgLine = `${cfg.command || '?'} ${(cfg.args||[]).join(' ')}`.trim();
            const cfgEl = document.createElement('div');
            cfgEl.textContent = cfgLine.length > 80 ? cfgLine.slice(0, 77) + '…' : cfgLine;
            cfgEl.style.opacity = '.65';
            meta.appendChild(cfgEl);
            if (st && st.ok) {
                const tNames = (st.tools||[]).map(t => t.name).slice(0, 5).join(', ');
                const more = (st.tools||[]).length > 5 ? ` +${(st.tools||[]).length - 5} more` : '';
                const toolsEl = document.createElement('div');
                toolsEl.textContent = `${(st.tools||[]).length} tools: ${tNames}${more}`;
                meta.appendChild(toolsEl);
            } else if (st && !st.ok) {
                const errEl = document.createElement('div');
                errEl.textContent = st.error || 'Connection failed';
                errEl.style.color = 'var(--danger)';
                meta.appendChild(errEl);
            }
            info.appendChild(nameEl);
            info.appendChild(meta);
            const actions = document.createElement('div');
            actions.className = 'mcp-server-actions';
            if (name === 'hue-bridge' && _MCP_PRESETS['hue-bridge']) {
                const setupBtn = document.createElement('button');
                setupBtn.type = 'button';
                setupBtn.className = 'btn btn-secondary';
                setupBtn.textContent = 'Setup';
                setupBtn.style.padding = '4px 8px';
                setupBtn.style.fontSize = '.75rem';
                setupBtn.addEventListener('click', () => _mcpShowSetupForm('hue-bridge', _MCP_PRESETS['hue-bridge']));
                actions.appendChild(setupBtn);
            }
            const removeBtn = document.createElement('button');
            removeBtn.type = 'button';
            removeBtn.className = 'remove';
            removeBtn.textContent = '✕';
            removeBtn.title = 'Remove server';
            removeBtn.addEventListener('click', () => _mcpRemoveServer(name));
            actions.appendChild(removeBtn);
            card.appendChild(dot);
            card.appendChild(info);
            card.appendChild(actions);
            mcpServerList.appendChild(card);
        }
    }

    async function _mcpFetchStatus() {
        try {
            const resp = await authFetch('/api/mcp/status');
            const data = await resp.json();
            _mcpStatus = {};
            for (const srv of (data.servers || [])) {
                _mcpStatus[srv.name] = { ok: srv.ok, error: srv.error, tools: srv.tools || [] };
            }
            _mcpRenderServerList();
        } catch { /* ignore */ }
    }

    function _mcpShowAddForm() {
        if (document.getElementById('mcp-add-form-el')) return;
        const form = document.createElement('div');
        form.className = 'mcp-add-form';
        form.id = 'mcp-add-form-el';
        form.innerHTML = `
            <div class="mcp-add-form-row">
                <input type="text" class="form-control" id="mcp-add-name" placeholder="Server name (e.g. my-server)" />
            </div>
            <div class="mcp-add-form-row">
                <input type="text" class="form-control" id="mcp-add-command" placeholder="Command (e.g. npx)" />
                <input type="text" class="form-control" id="mcp-add-args" placeholder='Args (comma-sep, e.g. -y, @pkg/name)' />
            </div>
            <div class="mcp-add-form-row">
                <input type="text" class="form-control" id="mcp-add-env" placeholder='Env JSON (opt, e.g. {"KEY":"val"})' />
            </div>
            <div class="mcp-add-form-btns">
                <button type="button" class="btn btn-secondary" id="mcp-add-cancel">Cancel</button>
                <button type="button" class="btn btn-primary" id="mcp-add-confirm">Add</button>
            </div>`;
        mcpAddCustomBtn.parentElement.insertBefore(form, mcpAddCustomBtn.nextSibling);
        mcpAddCustomBtn.style.display = 'none';
        document.getElementById('mcp-add-cancel').addEventListener('click', () => { form.remove(); mcpAddCustomBtn.style.display = ''; });
        document.getElementById('mcp-add-confirm').addEventListener('click', () => {
            const name = (document.getElementById('mcp-add-name').value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '');
            const cmd = (document.getElementById('mcp-add-command').value || '').trim();
            const argsRaw = (document.getElementById('mcp-add-args').value || '').trim();
            const envRaw = (document.getElementById('mcp-add-env').value || '').trim();
            if (!name || !cmd) { if (mcpServersNote) mcpServersNote.textContent = 'Name and command are required.'; return; }
            const args = argsRaw ? argsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
            let env = null;
            if (envRaw) { try { env = JSON.parse(envRaw); } catch { if (mcpServersNote) mcpServersNote.textContent = 'Invalid env JSON.'; return; } }
            const cfg = { command: cmd, args };
            if (env) cfg.env = env;
            if (!_mcpAddServer(name, cfg)) { if (mcpServersNote) mcpServersNote.textContent = `Server '${name}' already exists.`; return; }
            if (mcpServersNote) mcpServersNote.textContent = '';
            form.remove();
            mcpAddCustomBtn.style.display = '';
        });
        document.getElementById('mcp-add-name').focus();
    }

    async function _mcpOpenDirBrowser(input, row) {
        // Toggle off if already open
        let frame = row.querySelector('.dir-browser-frame');
        if (frame) { frame.remove(); return; }
        frame = document.createElement('div');
        frame.className = 'dir-browser-frame';
        row.appendChild(frame);

        const ICONS = {
            folder: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>',
            drive:  '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3 1h6v4H5V6zm8 0h2v4h-2V6zM5 12h10v2H5v-2z" clip-rule="evenodd"/></svg>',
            chevron:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 3l5 5-5 5"/></svg>',
            empty:  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/><path d="M12 11v4m0 0l-2-2m2 2l2-2"/></svg>',
        };

        // --- Build header ---
        const header = document.createElement('div');
        header.className = 'dir-browser-header';
        const backBtn = document.createElement('button');
        backBtn.className = 'dir-browser-back';
        backBtn.innerHTML = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 3L5 8l5 5"/></svg>';
        backBtn.disabled = true;
        header.appendChild(backBtn);
        const pathBar = document.createElement('div');
        pathBar.className = 'dir-browser-path';
        header.appendChild(pathBar);
        const selectBtn = document.createElement('button');
        selectBtn.className = 'dir-browser-select';
        selectBtn.textContent = 'Select';
        selectBtn.style.display = 'none';
        header.appendChild(selectBtn);
        frame.appendChild(header);

        // --- List container ---
        const list = document.createElement('div');
        list.className = 'dir-browser-list';
        frame.appendChild(list);

        let _parentPath = null;

        function renderPath(currentPath) {
            pathBar.innerHTML = '';
            if (!currentPath) {
                const seg = document.createElement('span');
                seg.className = 'dir-browser-path-seg';
                seg.textContent = 'This PC';
                pathBar.appendChild(seg);
                return;
            }
            // Split into segments: handle both C:\ and /unix
            const raw = currentPath.replace(/\\/g, '/');
            const parts = raw.split('/').filter(Boolean);
            // If Windows drive, first part has ':'
            for (let i = 0; i < parts.length; i++) {
                if (i > 0) {
                    const sep = document.createElement('span');
                    sep.className = 'dir-browser-path-sep';
                    sep.textContent = '/';
                    pathBar.appendChild(sep);
                }
                const seg = document.createElement('span');
                seg.className = 'dir-browser-path-seg';
                seg.textContent = parts[i];
                if (i < parts.length - 1) {
                    // Clickable: navigate to this ancestor
                    const ancestorPath = parts.slice(0, i + 1).join('/');
                    // Re-add drive colon prefix if needed (C: -> C:\)
                    seg.addEventListener('click', () => loadDir(ancestorPath));
                }
                pathBar.appendChild(seg);
            }
        }

        async function loadDir(dirPath) {
            list.innerHTML = '<div class="dir-browser-empty"><div class="dir-browser-spinner"></div></div>';
            try {
                const resp = await authFetch('/api/browse-dirs?path=' + encodeURIComponent(dirPath || ''));
                const data = await resp.json();
                list.innerHTML = '';

                const isRoot = !data.current;
                _parentPath = data.parent;
                backBtn.disabled = (data.parent === null || data.parent === undefined);
                selectBtn.style.display = isRoot ? 'none' : '';
                renderPath(data.current);

                selectBtn.onclick = () => {
                    input.value = data.current;
                    input.dispatchEvent(new Event('input', { bubbles: true }));
                    frame.remove();
                };
                backBtn.onclick = () => { if (_parentPath !== null && _parentPath !== undefined) loadDir(_parentPath); };

                if (!data.dirs.length) {
                    list.innerHTML = '<div class="dir-browser-empty">' + ICONS.empty + '<div>No subdirectories</div></div>';
                    return;
                }
                for (const name of data.dirs) {
                    const item = document.createElement('div');
                    item.className = 'dir-browser-item';
                    // Icon
                    const iconW = document.createElement('span');
                    iconW.className = 'dir-browser-item-icon' + (isRoot ? ' drive-icon' : '');
                    iconW.innerHTML = isRoot ? ICONS.drive : ICONS.folder;
                    item.appendChild(iconW);
                    // Name
                    const nameEl = document.createElement('span');
                    nameEl.className = 'dir-browser-item-name';
                    nameEl.textContent = name;
                    item.appendChild(nameEl);
                    // Chevron
                    const chev = document.createElement('span');
                    chev.className = 'dir-chevron';
                    chev.innerHTML = ICONS.chevron;
                    item.appendChild(chev);

                    const fullPath = data.current ? (data.current.replace(/[\\/]$/, '') + '/' + name) : name;
                    item.addEventListener('click', () => loadDir(fullPath));
                    list.appendChild(item);
                }
            } catch {
                list.innerHTML = '<div class="dir-browser-empty">Could not load directories</div>';
            }
        }
        await loadDir(input.value.trim());
    }

    function _mcpShowSetupForm(presetId, preset) {
        // Remove any existing setup form
        const old = document.getElementById('mcp-setup-form-el');
        if (old) old.remove();

        const existingServers = _mcpGetServersObj();
        const existingCfg = existingServers[presetId] || null;
        const existingEnv = (existingCfg && typeof existingCfg.env === 'object' && existingCfg.env) ? existingCfg.env : {};
        const existingArgs = Array.isArray(existingCfg?.args) ? existingCfg.args : [];

        const form = document.createElement('div');
        form.className = 'mcp-add-form';
        form.id = 'mcp-setup-form-el';

        const titleEl = document.createElement('div');
        titleEl.className = 'mcp-setup-title';
        titleEl.textContent = `Configure ${preset.label.replace(/^\S+\s*/, '')}`;
        form.appendChild(titleEl);

        const requiresNpx = String(preset.command || '').trim().toLowerCase() === 'npx';
        if (requiresNpx) {
            const helper = document.createElement('div');
            helper.className = 'settings-note';
            helper.style.margin = '4px 0 8px';
            helper.textContent = runtimeCapabilities.npx
                ? `npx gevonden${runtimeCapabilities.npx_path ? `: ${runtimeCapabilities.npx_path}` : ''}`
                : 'Node.js/npx nog niet gevonden. Zonder Node.js kan deze MCP-server niet starten.';
            helper.style.color = runtimeCapabilities.npx ? '' : 'var(--danger)';
            form.appendChild(helper);

            if (!runtimeCapabilities.npx) {
                const installRow = document.createElement('div');
                installRow.className = 'mcp-add-form-btns';
                installRow.style.justifyContent = 'flex-start';

                const copyBtn = document.createElement('button');
                copyBtn.type = 'button';
                copyBtn.className = 'btn btn-secondary';
                copyBtn.textContent = 'Copy Node install command';
                copyBtn.addEventListener('click', async () => {
                    const cmd = 'winget install OpenJS.NodeJS.LTS -e';
                    try {
                        await navigator.clipboard.writeText(cmd);
                        copyBtn.textContent = 'Copied';
                        setTimeout(() => { copyBtn.textContent = 'Copy Node install command'; }, 1500);
                    } catch {
                        copyBtn.textContent = cmd;
                    }
                });

                installRow.appendChild(copyBtn);
                form.appendChild(installRow);
            }
        }

        if (presetId === 'home-assistant') {
            const helper = document.createElement('div');
            helper.className = 'settings-note';
            helper.style.margin = '4px 0 8px';
            const uvxOk = !!runtimeCapabilities.uvx;
            helper.innerHTML = uvxOk
                ? '✅ uvx gevonden. Vul hieronder je Home Assistant URL en token in.'
                : '⚠️ uvx nog niet gevonden. Installeer eerst uvx, daarna werkt deze koppeling direct.';
            form.appendChild(helper);

            if (!uvxOk) {
                const installRow = document.createElement('div');
                installRow.className = 'mcp-add-form-btns';
                installRow.style.justifyContent = 'flex-start';

                const copyBtn = document.createElement('button');
                copyBtn.type = 'button';
                copyBtn.className = 'btn btn-secondary';
                copyBtn.textContent = 'Copy uv install command';
                copyBtn.addEventListener('click', async () => {
                    const cmd = 'winget install astral-sh.uv -e';
                    try {
                        await navigator.clipboard.writeText(cmd);
                        copyBtn.textContent = 'Copied';
                        setTimeout(() => { copyBtn.textContent = 'Copy uv install command'; }, 1500);
                    } catch {
                        copyBtn.textContent = cmd;
                    }
                });

                installRow.appendChild(copyBtn);
                form.appendChild(installRow);

                const steps = document.createElement('p');
                steps.className = 'settings-note';
                steps.style.margin = '2px 0 8px';
                steps.textContent = 'Install stappen: 1) run het commando in PowerShell, 2) herstart terminal/app, 3) open deze setup opnieuw.';
                form.appendChild(steps);
            }
        }

        for (const field of preset.setup) {
            const row = document.createElement('div');
            row.className = 'mcp-add-form-row';
            const label = document.createElement('label');
            label.className = 'mcp-setup-label';
            label.textContent = field.label + (field.optional ? ' (optional)' : '');
            const input = document.createElement('input');
            input.type = field.key.toLowerCase().includes('token') || field.key.toLowerCase().includes('key') || field.key.toLowerCase().includes('pass') ? 'password' : 'text';
            input.className = 'form-control';
            input.placeholder = field.placeholder || '';
            input.dataset.setupKey = field.key;
            input.dataset.setupType = field.type;
            if (field.argIndex !== undefined) input.dataset.argIndex = field.argIndex;
            if (field.envTemplate) input.dataset.envTemplate = field.envTemplate;
            if (field.envKey) input.dataset.envKey = field.envKey;
            if (field.optional) input.dataset.optional = '1';
            // Prefill with existing saved server values so users don't have to re-link.
            if (field.type === 'env') {
                const envKey = field.envKey || field.key;
                if (typeof existingEnv[envKey] === 'string') {
                    input.value = existingEnv[envKey];
                }
            } else if (field.type === 'arg' && field.argIndex !== undefined) {
                const idx = Number(field.argIndex);
                if (Number.isFinite(idx) && typeof existingArgs[idx] === 'string') {
                    input.value = existingArgs[idx];
                }
            }
            row.appendChild(label);
            if (field.browse) {
                const wrap = document.createElement('div');
                wrap.className = 'dir-browser-wrap';
                wrap.appendChild(input);
                const browseBtn = document.createElement('button');
                browseBtn.type = 'button';
                browseBtn.className = 'btn btn-secondary';
                browseBtn.textContent = 'Browse…';
                browseBtn.addEventListener('click', () => _mcpOpenDirBrowser(input, row));
                wrap.appendChild(browseBtn);
                row.appendChild(wrap);
            } else {
                row.appendChild(input);
            }
            form.appendChild(row);
        }

        if (presetId === 'hue-bridge') {
            const hueHelp = document.createElement('p');
            hueHelp.className = 'settings-note';
            hueHelp.style.margin = '2px 0 0';
            hueHelp.textContent = 'Flow: 1) Discover, 2) press physical Hue Bridge button, 3) Create Key, 4) Check Key, 5) Reachable Devices.';
            form.appendChild(hueHelp);

            const toolsRow = document.createElement('div');
            toolsRow.className = 'mcp-add-form-btns';
            toolsRow.style.justifyContent = 'flex-start';

            const discoverBtn = document.createElement('button');
            discoverBtn.type = 'button';
            discoverBtn.className = 'btn btn-secondary';
            discoverBtn.textContent = 'Discover';

            const createKeyBtn = document.createElement('button');
            createKeyBtn.type = 'button';
            createKeyBtn.className = 'btn btn-secondary';
            createKeyBtn.textContent = 'Create Key';

            const checkKeyBtn = document.createElement('button');
            checkKeyBtn.type = 'button';
            checkKeyBtn.className = 'btn btn-secondary';
            checkKeyBtn.textContent = 'Check';

            const devicesBtn = document.createElement('button');
            devicesBtn.type = 'button';
            devicesBtn.className = 'btn btn-secondary';
            devicesBtn.textContent = 'Reachable Devices';

            const resultBox = document.createElement('pre');
            resultBox.className = 'form-control';
            resultBox.style.marginTop = '8px';
            resultBox.style.whiteSpace = 'pre-wrap';
            resultBox.style.maxHeight = '200px';
            resultBox.style.overflow = 'auto';
            resultBox.style.fontSize = '.78rem';
            resultBox.textContent = '';

            const statusLine = document.createElement('p');
            statusLine.className = 'settings-note';
            statusLine.style.margin = '6px 0 0';
            statusLine.textContent = '';

            const setBusy = (busy) => {
                discoverBtn.disabled = busy;
                createKeyBtn.disabled = busy;
                checkKeyBtn.disabled = busy;
                devicesBtn.disabled = busy;
            };
            const showResult = (obj) => {
                try { resultBox.textContent = JSON.stringify(obj, null, 2); }
                catch { resultBox.textContent = String(obj || ''); }
            };
            const byKey = (k) => form.querySelector(`input[data-setup-key="${k}"]`);

            const collectHueCfg = () => {
                const cfg = { command: preset.command, args: [...preset.args], env: { ...(preset.env || {}) } };
                const ip = (byKey('HUE_BRIDGE_IP')?.value || '').trim();
                const url = (byKey('HUE_BRIDGE_URL')?.value || '').trim();
                const key = (byKey('HUE_APPLICATION_KEY')?.value || '').trim();
                if (ip) cfg.env.HUE_BRIDGE_IP = ip;
                if (url) cfg.env.HUE_BRIDGE_URL = url;
                if (key) cfg.env.HUE_APPLICATION_KEY = key;
                return cfg;
            };

            const parseHostFromUrl = (rawUrl) => {
                const value = (rawUrl || '').trim();
                if (!value) return '';
                try {
                    const normalized = value.includes('://') ? value : `http://${value}`;
                    const u = new URL(normalized);
                    return (u.hostname || '').trim();
                } catch {
                    return '';
                }
            };

            let _hueDiscovered = false;
            const syncHueStepState = () => {
                const ip = (byKey('HUE_BRIDGE_IP')?.value || '').trim();
                const url = (byKey('HUE_BRIDGE_URL')?.value || '').trim();
                const key = (byKey('HUE_APPLICATION_KEY')?.value || '').trim();
                const hostFromUrl = parseHostFromUrl(url);
                const hasBridgeTarget = !!(ip || hostFromUrl);
                createKeyBtn.disabled = !_hueDiscovered && !hasBridgeTarget;
                checkKeyBtn.disabled = !hasBridgeTarget || !key;
                devicesBtn.disabled = !hasBridgeTarget || !key;
            };

            const manualUrlRow = byKey('HUE_BRIDGE_URL')?.closest('.mcp-add-form-row');
            if (manualUrlRow) {
                const existingManualUrl = (byKey('HUE_BRIDGE_URL')?.value || '').trim();
                manualUrlRow.style.display = existingManualUrl ? '' : 'none';
            }
            byKey('HUE_BRIDGE_IP')?.addEventListener('input', syncHueStepState);
            byKey('HUE_BRIDGE_URL')?.addEventListener('input', syncHueStepState);
            byKey('HUE_APPLICATION_KEY')?.addEventListener('input', syncHueStepState);

            const ensureHueServerReady = async () => {
                const cfg = collectHueCfg();
                const bridgeIp = ((cfg.env || {}).HUE_BRIDGE_IP || '').trim();
                const bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                if (!bridgeIp && !bridgeUrl) {
                    throw new Error('Bridge target is required. Click Discover first, or enter Manual Bridge URL.');
                }
                _mcpUpsertServer('hue-bridge', cfg);
                await _mcpPersistServersOnly();
                const refreshResp = await authFetch('/api/mcp/refresh', { method: 'POST' });
                if (!refreshResp.ok) throw new Error('Could not refresh MCP servers.');
                await _mcpFetchStatus();
                const st = _mcpStatus['hue-bridge'];
                if (!st || !st.ok) {
                    throw new Error((st && st.error) || 'Hue MCP server is not connected yet.');
                }
                return cfg;
            };

            const discoverBridgeIp = async () => {
                const cfg = collectHueCfg();
                _mcpUpsertServer('hue-bridge', cfg);
                await _mcpPersistServersOnly();
                const refreshResp = await authFetch('/api/mcp/refresh', { method: 'POST' });
                if (!refreshResp.ok) throw new Error('Could not refresh MCP servers.');
                await _mcpFetchStatus();
                const data = await _mcpCallTool('hue-bridge', 'hue_discover_bridges', {});
                const out = data.result ?? data.result_text;
                const bridges = Array.isArray(out?.bridges) ? out.bridges : [];
                const first = bridges.find(b => (b && b.ip)) || null;
                if (first && byKey('HUE_BRIDGE_IP')) {
                    byKey('HUE_BRIDGE_IP').value = String(first.ip || '').trim();
                    _hueDiscovered = true;
                }
                syncHueStepState();
                return { out, bridgeIp: first ? String(first.ip || '').trim() : '' };
            };

            discoverBtn.addEventListener('click', async () => {
                setBusy(true);
                try {
                    const { out, bridgeIp } = await discoverBridgeIp();
                    if (!bridgeIp) {
                        _hueDiscovered = false;
                        if (manualUrlRow) manualUrlRow.style.display = '';
                        statusLine.textContent = 'No bridge found via discovery. Use Manual Bridge URL.';
                        showResult({
                            ...(out || {}),
                            hint: 'No Hue Bridge found via discovery. Enter Manual Bridge URL and continue with Create Key.',
                        });
                    } else {
                        const cfg = collectHueCfg();
                        _mcpUpsertServer('hue-bridge', cfg);
                        await _mcpPersistServersOnly();
                        if (manualUrlRow) manualUrlRow.style.display = 'none';
                        statusLine.textContent = `Bridge discovered (${bridgeIp}) and saved.`;
                        resultBox.textContent = '';
                    }
                    syncHueStepState();
                } catch (e) {
                    statusLine.textContent = 'Discovery failed.';
                    showResult({ ok: false, error: String((e && e.message) || e) });
                } finally {
                    setBusy(false);
                }
            });

            createKeyBtn.addEventListener('click', async () => {
                setBusy(true);
                try {
                    let cfg = await ensureHueServerReady();
                    let bridgeIp = ((cfg.env || {}).HUE_BRIDGE_IP || '').trim();
                    let bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    if (!bridgeIp) {
                        if (!bridgeUrl) {
                            const discovered = await discoverBridgeIp();
                            bridgeIp = discovered.bridgeIp;
                        }
                        if (!bridgeIp && !bridgeUrl) {
                            throw new Error('No Hue Bridge found via discovery. Enter Manual Bridge URL.');
                        }
                        cfg = collectHueCfg();
                        bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    }
                    const data = await _mcpCallTool('hue-bridge', 'hue_create_application_key', {
                        bridge_ip: bridgeIp,
                        bridge_url: bridgeUrl,
                    });
                    const out = data.result ?? data.result_text;
                    if (out && typeof out === 'object' && out.application_key && byKey('HUE_APPLICATION_KEY')) {
                        byKey('HUE_APPLICATION_KEY').value = out.application_key;
                        const updatedCfg = collectHueCfg();
                        _mcpUpsertServer('hue-bridge', updatedCfg);
                        await _mcpPersistServersOnly();
                    }
                    syncHueStepState();
                    showResult(out);
                } catch (e) {
                    showResult({ ok: false, error: String((e && e.message) || e) });
                } finally {
                    setBusy(false);
                }
            });

            checkKeyBtn.addEventListener('click', async () => {
                setBusy(true);
                try {
                    let cfg = collectHueCfg();
                    let bridgeIp = ((cfg.env || {}).HUE_BRIDGE_IP || '').trim();
                    let bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    const appKey = ((cfg.env || {}).HUE_APPLICATION_KEY || '').trim();
                    if (!bridgeIp) {
                        if (!bridgeUrl) {
                            const discovered = await discoverBridgeIp();
                            bridgeIp = discovered.bridgeIp;
                        }
                        if (!bridgeIp && !bridgeUrl) {
                            throw new Error('No Hue Bridge found via discovery. Enter Manual Bridge URL.');
                        }
                        cfg = collectHueCfg();
                        bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    }
                    if (!appKey) throw new Error('Application Key is required.');
                    _mcpUpsertServer('hue-bridge', cfg);
                    await _mcpPersistServersOnly();
                    const refreshResp = await authFetch('/api/mcp/refresh', { method: 'POST' });
                    if (!refreshResp.ok) throw new Error('Could not refresh MCP servers.');
                    await _mcpFetchStatus();
                    const validate = await _mcpCallTool('hue-bridge', 'hue_validate_application_key', {
                        bridge_ip: bridgeIp,
                        bridge_url: bridgeUrl,
                        application_key: appKey,
                    });
                    const lights = await _mcpCallTool('hue-bridge', 'hue_list_lights', {
                        bridge_ip: bridgeIp,
                        bridge_url: bridgeUrl,
                        application_key: appKey,
                    });
                    showResult({
                        validate: validate.result ?? validate.result_text,
                        lights_preview: lights.result ?? lights.result_text,
                    });
                } catch (e) {
                    showResult({ ok: false, error: String((e && e.message) || e) });
                } finally {
                    setBusy(false);
                }
            });

            devicesBtn.addEventListener('click', async () => {
                setBusy(true);
                try {
                    let cfg = collectHueCfg();
                    let bridgeIp = ((cfg.env || {}).HUE_BRIDGE_IP || '').trim();
                    let bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    const appKey = ((cfg.env || {}).HUE_APPLICATION_KEY || '').trim();
                    if (!bridgeIp && !bridgeUrl) {
                        const discovered = await discoverBridgeIp();
                        bridgeIp = discovered.bridgeIp;
                        cfg = collectHueCfg();
                        bridgeUrl = ((cfg.env || {}).HUE_BRIDGE_URL || '').trim();
                    }
                    if (!bridgeIp && !bridgeUrl) {
                        throw new Error('No Hue Bridge found via discovery. Enter Manual Bridge URL.');
                    }
                    if (!appKey) throw new Error('Application Key is required.');

                    _mcpUpsertServer('hue-bridge', cfg);
                    await _mcpPersistServersOnly();
                    const refreshResp = await authFetch('/api/mcp/refresh', { method: 'POST' });
                    if (!refreshResp.ok) throw new Error('Could not refresh MCP servers.');
                    await _mcpFetchStatus();

                    const lights = await _mcpCallTool('hue-bridge', 'hue_list_lights', {
                        bridge_ip: bridgeIp,
                        bridge_url: bridgeUrl,
                        application_key: appKey,
                    });
                    const out = lights.result ?? lights.result_text;
                    const devices = Array.isArray(out?.lights) ? out.lights : [];
                    const reachable = devices.filter(d => d && d.reachable === true).length;
                    showResult({
                        ok: true,
                        total_devices: devices.length,
                        reachable_devices: reachable,
                        unreachable_devices: Math.max(0, devices.length - reachable),
                        devices,
                    });
                } catch (e) {
                    showResult({ ok: false, error: String((e && e.message) || e) });
                } finally {
                    setBusy(false);
                }
            });

            toolsRow.appendChild(discoverBtn);
            toolsRow.appendChild(createKeyBtn);
            toolsRow.appendChild(checkKeyBtn);
            toolsRow.appendChild(devicesBtn);
            form.appendChild(toolsRow);
            form.appendChild(statusLine);
            form.appendChild(resultBox);

            // Default behavior: try discovery immediately, and only fall back to manual IP if none found.
            setTimeout(async () => {
                setBusy(true);
                try {
                    const { out, bridgeIp } = await discoverBridgeIp();
                    if (!bridgeIp) {
                        _hueDiscovered = false;
                        if (manualUrlRow) manualUrlRow.style.display = '';
                        statusLine.textContent = 'No bridge found via discovery. Use Manual Bridge URL.';
                        showResult({
                            ...(out || {}),
                            hint: 'Auto-discovery returned no bridge. Enter Manual Bridge URL.',
                        });
                    } else {
                        _hueDiscovered = true;
                        if (manualUrlRow) manualUrlRow.style.display = 'none';
                        statusLine.textContent = `Bridge discovered (${bridgeIp}) and IP auto-filled.`;
                        resultBox.textContent = '';
                    }
                    syncHueStepState();
                } catch (e) {
                    _hueDiscovered = false;
                    if (manualUrlRow) manualUrlRow.style.display = '';
                    statusLine.textContent = 'Auto-discovery failed. Use Manual Bridge URL if needed.';
                    showResult({
                        ok: false,
                        error: String((e && e.message) || e),
                        hint: 'If discovery fails, enter Manual Bridge URL.',
                    });
                } finally {
                    setBusy(false);
                }
            }, 50);

            syncHueStepState();
        }

        if (preset.note) {
            const noteEl = document.createElement('p');
            noteEl.className = 'settings-note';
            noteEl.style.margin = '2px 0 0';
            noteEl.textContent = preset.note;
            form.appendChild(noteEl);
        }

        const btns = document.createElement('div');
        btns.className = 'mcp-add-form-btns';
        const cancelBtn = document.createElement('button');
        cancelBtn.type = 'button';
        cancelBtn.className = 'btn btn-secondary';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => form.remove());
        const addBtn = document.createElement('button');
        addBtn.type = 'button';
        addBtn.className = 'btn btn-primary';
        addBtn.textContent = (_mcpGetServersObj()[presetId] ? 'Save Server' : 'Add Server');
        addBtn.addEventListener('click', () => {
            if (!_mcpRuntimeCommandAvailable(preset.command)) {
                _mcpShowMissingRuntimeHint(preset.command, mcpServersNote);
                return;
            }
            const cfg = { command: preset.command, args: [...preset.args] };
            if (preset.env) cfg.env = { ...preset.env };

            // Collect field values
            const inputs = form.querySelectorAll('input[data-setup-key]');
            for (const inp of inputs) {
                const val = inp.value.trim();
                const isOptional = inp.dataset.optional === '1';
                if (!val && !isOptional) {
                    inp.style.borderColor = 'var(--danger)';
                    inp.focus();
                    return;
                }
                if (inp.dataset.setupType === 'env') {
                    if (!cfg.env) cfg.env = {};
                    if (inp.dataset.envTemplate) {
                        const envKey = inp.dataset.envKey || inp.dataset.setupKey;
                        cfg.env[envKey] = inp.dataset.envTemplate.replace('{{value}}', val);
                    } else {
                        cfg.env[inp.dataset.setupKey] = val;
                    }
                } else if (inp.dataset.setupType === 'arg') {
                    const idx = parseInt(inp.dataset.argIndex, 10);
                    cfg.args[idx] = val;
                }
            }

            _mcpUpsertServer(presetId, cfg);
            form.remove();
        });
        btns.appendChild(cancelBtn);
        btns.appendChild(addBtn);
        form.appendChild(btns);

        // Insert after the preset gallery
        mcpPresetGallery.parentElement.insertBefore(form, mcpPresetGallery.nextSibling);
        const firstInput = form.querySelector('input[data-setup-key]');
        if (firstInput) firstInput.focus();
    }

    function _mcpInit() {
        if (mcpPresetGallery) {
            mcpPresetGallery.addEventListener('click', (e) => {
                const chip = e.target.closest('.mcp-preset-chip');
                if (!chip) return;
                const id = chip.dataset.preset;
                const preset = _MCP_PRESETS[id];
                if (!preset) return;
                if (!_mcpRuntimeCommandAvailable(preset.command)) {
                    _mcpShowMissingRuntimeHint(preset.command, mcpServersNote);
                    return;
                }
                // If preset has setup fields, show setup form
                if (preset.setup && preset.setup.length) {
                    _mcpShowSetupForm(id, preset);
                    return;
                }
                // No setup needed — add immediately
                _mcpAddServer(id, preset);
            });
        }
        if (mcpAddCustomBtn) mcpAddCustomBtn.addEventListener('click', _mcpShowAddForm);
        // Sync textarea changes → server list
        if (mcpServersInput) mcpServersInput.addEventListener('input', () => { _mcpRenderServerList(); _mcpUpdatePresetChips(); });
        // Initial render after settings load (deferred since loadSettings is async)
        setTimeout(() => { _mcpRenderServerList(); _mcpUpdatePresetChips(); _mcpFetchStatus(); }, 1500);
    }

    /* ── Webhooks helpers ─────────────────────────────────── */
    function _whRenderList(webhooks) {
        if (!webhookList) return;
        webhookList.innerHTML = '';
        (webhooks || []).forEach((wh, idx) => {
            const clone = webhookTemplate.content.cloneNode(true);
            const item = clone.querySelector('.webhook-item');
            item.dataset.idx = idx;
            item.querySelector('.wh-name').value = wh.name || '';
            item.querySelector('.wh-url').value = wh.url || '';
            const methodSel = item.querySelector('.wh-method');
            methodSel.value = (wh.method || 'GET').toUpperCase();
            item.querySelector('.wh-auth-type').value = wh.auth_type || 'none';
            item.querySelector('.wh-auth-value').value = wh.auth_value || '';
            item.querySelector('.wh-description').value = wh.description || '';
            // toggle auth field visibility
            const authVal = item.querySelector('.wh-auth-value');
            authVal.style.display = (wh.auth_type && wh.auth_type !== 'none') ? '' : 'none';
            item.querySelector('.wh-auth-type').addEventListener('change', (e) => {
                authVal.style.display = e.target.value !== 'none' ? '' : 'none';
            });
            item.querySelector('.wh-remove').addEventListener('click', () => {
                item.remove();
            });
            webhookList.appendChild(clone);
        });
    }

    function _whCollect() {
        if (!webhookList) return [];
        const items = webhookList.querySelectorAll('.webhook-item');
        const result = [];
        items.forEach(item => {
            const name = item.querySelector('.wh-name').value.trim();
            const url = item.querySelector('.wh-url').value.trim();
            if (!name || !url) return; // skip incomplete entries
            result.push({
                name,
                url,
                method: item.querySelector('.wh-method').value || 'GET',
                auth_type: item.querySelector('.wh-auth-type').value || 'none',
                auth_value: item.querySelector('.wh-auth-value').value.trim(),
                description: item.querySelector('.wh-description').value.trim(),
            });
        });
        return result;
    }

    function _whInit() {
        if (webhookAddBtn && webhookTemplate) {
            webhookAddBtn.addEventListener('click', () => {
                const current = _whCollect();
                current.push({ name: '', url: '', method: 'GET', auth_type: 'none', auth_value: '', description: '' });
                _whRenderList(current);
                // Focus the new name field
                const items = webhookList.querySelectorAll('.webhook-item');
                const last = items[items.length - 1];
                if (last) last.querySelector('.wh-name').focus();
            });
        }
    }

    function _checkScheduleFailures() {
        setTimeout(async () => {
            try {
                const data = await authFetchJson(`${BACKEND_URL}/api/schedule/failures`);
                const failures = data.failures || [];
                if (failures.length === 0) return;
                for (const failure of failures.slice(0, 3)) {
                    showToast('warning', `Scheduled task failed: ${failure.goal || 'unknown'} — ${failure.error || ''}`, 10000);
                }
                await authFetch(`${BACKEND_URL}/api/schedule/failures/ack`, { method: 'POST' });
            } catch (_) {}
        }, 5000);
    }

    let _authorizedStartupStarted = false;
    let _authorizedStartupRetryTimer = null;

    async function _startAuthorizedStartup() {
        if (_authorizedStartupStarted || _pageClosing) return;
        if (!_getAuthToken()) {
            try {
                await _refreshAuthToken({ preferDiscovery: true });
            } catch (_) {}
        }
        if (!_getAuthToken()) {
            setStatus('Connecting…', 'running', true);
            if (!_authorizedStartupRetryTimer) {
                _authorizedStartupRetryTimer = setTimeout(() => {
                    _authorizedStartupRetryTimer = null;
                    void _startAuthorizedStartup();
                }, 2000);
            }
            return;
        }

        _authorizedStartupStarted = true;
        connectWebSocket();
        _startBrowserPreviewPolling();
        loadSettings();
        loadModels();
        loadOllamaLibrary();
        loadOllamaInstalled();
        loadSessions();
        _mcpInit();
        _whInit();
        _checkScheduleFailures();
    }

    _initAppRuntimeModule();
    _initAppSessionModule();
    _refreshWorkflowIndicator();
    _refreshComposerModeUi();
    _initScheduleTimers();
    _scheduleUiUnlockSweep('startup');
    _startClientHeartbeat();
    _startRecoveryWatchdog();
    _restoreSession();
    await _startAuthorizedStartup();
    loadVersionInfo();
    checkForUpdate(false);

    const _dropLiveLanesForSession = (sessionId) => {
        const target = String(sessionId || '').trim();
        if (!target) return [];
        const removed = [];
        for (const [rid, run] of Array.from(runs.entries())) {
            if (_isHistoryRunId(rid)) continue;
            if (String(run?._sessionId || '').trim() !== target) continue;
            removed.push(rid);
            _discardRunLane(rid, false);
        }
        if (removed.length > 0) {
            _currentHandlingRunId = '';
        }
        return removed;
    };

    if (window.__AO_TEST_HOOKS__ && typeof window.__AO_TEST_HOOKS__ === 'object') {
        Object.assign(window.__AO_TEST_HOOKS__, {
            activateContinueSession: _activateContinueSession,
            replaySessionLane,
            loadSessions,
            dropLiveLanesForSession: _dropLiveLanesForSession,
        });
    }

    // Refresh models button
    if (refreshModelsBtn) refreshModelsBtn.addEventListener('click', async () => {
        refreshModelsBtn.disabled = true;
        refreshModelsBtn.querySelector('svg')?.classList.add('spin');
        await loadModels(true);
        await loadOllamaLibrary();
        await loadOllamaInstalled();
        refreshModelsBtn.disabled = false;
        refreshModelsBtn.querySelector('svg')?.classList.remove('spin');
    });

    // Auto-refresh session list every 60 seconds (picks up Slack-triggered runs etc.)
    setInterval(loadSessions, 60_000);
    // Periodic update check every 6 hours
    setInterval(() => checkForUpdate(false), 6 * 60 * 60 * 1000);

    window.addEventListener('pagehide', () => {
        _pageClosing = true;
        if (_clientHeartbeatTimer) { clearInterval(_clientHeartbeatTimer); _clientHeartbeatTimer = null; }
        if (_recoveryWatchdogTimer) { clearInterval(_recoveryWatchdogTimer); _recoveryWatchdogTimer = null; }
        _disposeBrowserPreview();
        _disposeWhatsApp();
        // Never send immediate shutdown on unload — can't distinguish refresh from tab close.
        // Instead, send a disconnect with stop_on_close hint; the launcher will handle the grace period.
        _disconnectLauncherClient(_stopOnClose && _isLastClient() ? 'close-stop' : 'pagehide');
    });
    window.addEventListener('beforeunload', () => {
        _pageClosing = true;
        if (_clientHeartbeatTimer) { clearInterval(_clientHeartbeatTimer); _clientHeartbeatTimer = null; }
        if (_recoveryWatchdogTimer) { clearInterval(_recoveryWatchdogTimer); _recoveryWatchdogTimer = null; }
        _disposeBrowserPreview();
        _disposeWhatsApp();
        _disconnectLauncherClient(_stopOnClose && _isLastClient() ? 'close-stop' : 'beforeunload');
    });

    if (browserPreviewRefreshBtn) {
        browserPreviewRefreshBtn.addEventListener('click', () => refreshBrowserPreview(true));
    }

    if (browserPreviewCollapseBtn) {
        browserPreviewCollapseBtn.addEventListener('click', () => {
            const collapsed = !!browserPreviewSection?.classList.contains('browser-preview-collapsed');
            setBrowserPreviewCollapsed(!collapsed);
        });
    }

    if (browserPreviewFullscreenBtn) {
        browserPreviewFullscreenBtn.addEventListener('click', () => {
            if (!browserPreviewSection) return;
            const isFs = browserPreviewSection.classList.toggle('browser-preview-fullscreen');
            browserPreviewFullscreenBtn.title = isFs ? 'Exit full screen' : 'Full screen browser view';
            // If entering fullscreen from collapsed, uncollapse
            if (isFs && browserPreviewSection.classList.contains('browser-preview-collapsed')) {
                setBrowserPreviewCollapsed(false);
            }
        });
    }
});