// Session Management Functions
// This module handles all session-related operations

import Storage from './storage.js';
import uiModule, { autoResize, styledPrompt } from './ui.js';
import chatRenderer from './chatRenderer.js?v=20260722ctxheader1';
import { providerLogo } from './providers.js';
import { initModelPicker, updateModelPicker } from './modelPicker.js?v=20260722ctxheader1';
import themeModule from './theme.js';
import spinnerModule from './spinner.js';

const API_BASE = window.location.origin;

let sessions = [];
let currentSessionId = null;
let _sessionNavToken = 0;
let _skipAutoSelect = false;
let _suppressNextSessionLoading = false;
let _rootFreshChatApplied = false;
const HISTORY_DISPLAY_CHAR_LIMIT = 160000;
const HISTORY_DISPLAY_TAIL_CHARS = 20000;
const HISTORY_PAGE_LIMIT_MOBILE = 8;
const HISTORY_PAGE_LIMIT_DESKTOP = 24;

const SIDEBAR_MAX_VISIBLE = 10;
const FOLDER_MAX_VISIBLE = 5;
let _showAllSessions = false;
let _expandedFolders = {};  // folderName -> true if "show more" clicked
let _sortMode = Storage.get('odysseus-session-sort') || 'active'; // default to last active
const DATE_SECTION_COLLAPSE_KEY = 'ody-session-date-section-collapsed';
let _autoCreateInProgress = false; // guard against recursive auto-create
const _INCOGNITO_SESSIONS_KEY = 'ody-incognito-sessions'; // sessionStorage key for incognito session IDs
const _isMac = /Mac|iPhone|iPad/.test(navigator.platform);
const _mod = _isMac ? '⌘' : 'Ctrl';
let _historyPager = null;

function _shouldPreserveStartupComposer(msgInput) {
  if (!msgInput || !msgInput.value) return false;
  if (window.__odysseusComposerUserEdited) return true;
  return !!document.getElementById('app-loader') && document.activeElement === msgInput;
}

function _clearComposerUnlessStartupTyped(msgInput) {
  if (!msgInput) return;
  if (_shouldPreserveStartupComposer(msgInput)) {
    msgInput.disabled = false;
    autoResize(msgInput);
    return;
  }
  msgInput.value = '';
}

function _paintSessionLoading(chatHistory, label = 'Loading chat') {
  if (!chatHistory) return;
  if (chatRenderer.hideWelcomeScreen) chatRenderer.hideWelcomeScreen();
  chatHistory.style.transition = '';
  chatHistory.style.opacity = '1';
  chatHistory.classList.add('no-animate');
  chatHistory.innerHTML = '';

  const wrap = document.createElement('div');
  wrap.className = 'session-loading-state session-loading-skeleton';
  wrap.setAttribute('role', 'status');
  wrap.setAttribute('aria-live', 'polite');
  wrap.setAttribute('aria-label', label);

  const viewportHeight = chatHistory.clientHeight || window.innerHeight || 720;
  const bubbleCount = Math.max(8, Math.min(16, Math.ceil(viewportHeight / 86)));
  for (let i = 0; i < bubbleCount; i += 1) {
    const bubble = document.createElement('div');
    bubble.className = `session-skeleton-bubble ${i % 2 ? 'is-user' : 'is-ai'}`;
    const lines = i % 4 === 1 ? 2 : (i % 4 === 3 ? 3 : 4);
    for (let j = 0; j < lines; j += 1) {
      const line = document.createElement('div');
      line.className = 'session-skeleton-line';
      line.style.width = `${[72, 92, 58, 82][(i + j) % 4]}%`;
      bubble.appendChild(line);
    }
    wrap.appendChild(bubble);
  }
  chatHistory.appendChild(wrap);
}

function _updateSessionLoading(chatHistory, label) {
  const el = chatHistory?.querySelector('.session-loading-state');
  if (el) el.setAttribute('aria-label', label);
}

function _nextPaint() {
  return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}

function _displayHistoryContent(content) {
  const text = String(content || '');
  if (text.length <= HISTORY_DISPLAY_CHAR_LIMIT) return text;
  const head = text.slice(0, HISTORY_DISPLAY_CHAR_LIMIT - HISTORY_DISPLAY_TAIL_CHARS);
  const tail = text.slice(-HISTORY_DISPLAY_TAIL_CHARS);
  const omitted = text.length - head.length - tail.length;
  return [
    `> Large message display clipped (${omitted.toLocaleString()} characters omitted). Full content remains stored in chat history/export.`,
    '',
    head,
    '',
    '```text',
    `[... ${omitted.toLocaleString()} characters omitted from on-screen history render ...]`,
    '```',
    '',
    tail,
  ].join('\n');
}

function _stripUserVisionBlocks(text) {
  return String(text || '').replace(
    /\n*\[Image: ([^\]]+)\]\n[\s\S]*?(?=\n*\[Image: |\n*\[Image attached: |\n*=== File: |\n*\[PDF content\]:|$)/g,
    ''
  ).trim();
}

function _historyPageLimit() {
  return window.innerWidth <= 768 ? HISTORY_PAGE_LIMIT_MOBILE : HISTORY_PAGE_LIMIT_DESKTOP;
}

function _historyUrl(id, { limit = null, offset = null } = {}) {
  const url = new URL(`${API_BASE}/api/history/${id}`);
  if (limit != null) url.searchParams.set('limit', String(limit));
  if (offset != null) url.searchParams.set('offset', String(offset));
  return url.toString();
}

function _addHistoryMessageWithFullRenderer(role, content, modelName, meta) {
  const box = document.getElementById('chat-history');
  if (!box) return [];
  const marker = document.createComment('history-message');
  box.appendChild(marker);
  let rendered = null;
  try {
    rendered = chatRenderer.addMessage(role, content, modelName, meta);
  } catch (e) {
    marker.remove();
    throw e;
  }
  const nodes = [];
  let node = marker.nextSibling;
  while (node) {
    const next = node.nextSibling;
    nodes.push(node);
    node = next;
  }
  marker.remove();
  return nodes.length ? nodes : (rendered ? [rendered] : []);
}

function _renderHistoryMessage(msg, modelName) {
  const meta = msg.metadata ? { ...msg.metadata, _fromHistory: true } : null;
  let displayContent;
  if (typeof msg.content === 'string') {
    displayContent = _displayHistoryContent(msg.content);
  } else if (Array.isArray(msg.content)) {
    displayContent = _displayHistoryContent(msg.content.filter(p => p.type === 'text').map(p => p.text).join('\n').trim());
  } else {
    displayContent = '';
  }
  if (msg.role === 'user') {
    displayContent = _stripUserVisionBlocks(displayContent);
    const trimmed = displayContent.trim();
    if (
      trimmed === 'Continue where you left off' ||
      trimmed.startsWith('Your message was cut off.') ||
      trimmed.startsWith('Your previous response was interrupted.') ||
      displayContent.includes('[Instruction: Rewrite') ||
      displayContent.includes('[Instruction: Explain')
    ) {
      return null;
    }
    const docEditMatch = displayContent.match(/^In the document, edit this specific text \((lines? [\d-]+)\):\n```\n([\s\S]*?)\n```\n\nInstruction: ([\s\S]*)$/);
    if (docEditMatch) {
      displayContent = `[Doc edit: ${docEditMatch[1]}] ${docEditMatch[3]}`;
    }
  }
  return _addHistoryMessageWithFullRenderer(msg.role, displayContent, modelName, meta);
}

function _clearHistoryPager() {
  const box = document.getElementById('chat-history');
  if (_historyPager?.handler && box) {
    box.removeEventListener('scroll', _historyPager.handler);
  }
  _historyPager = null;
}

function _installHistoryPager(id, pageInfo, modelName) {
  const box = document.getElementById('chat-history');
  _clearHistoryPager();
  if (!box || !pageInfo || !pageInfo.has_more_before) return;

  _historyPager = {
    sessionId: id,
    offset: Number(pageInfo.offset || 0),
    limit: Number(pageInfo.limit || _historyPageLimit()),
    loading: false,
    done: false,
    modelName,
    handler: null,
  };

  const loadOlder = async () => {
    if (!_historyPager || _historyPager.loading || _historyPager.done) return;
    if (_historyPager.sessionId !== currentSessionId) return;
    if (box.scrollTop > 90) return;

    const nextOffset = Math.max(0, _historyPager.offset - _historyPager.limit);
    const nextLimit = _historyPager.offset - nextOffset;
    if (nextLimit <= 0) {
      _historyPager.done = true;
      return;
    }

    _historyPager.loading = true;
    const anchor = box.querySelector('.msg, .agent-thread, .gallery-bubble');
    const beforeHeight = box.scrollHeight;
    try {
      const res = await fetch(_historyUrl(_historyPager.sessionId, { limit: nextLimit, offset: nextOffset }));
      const data = await res.json();
      if (!_historyPager || _historyPager.sessionId !== currentSessionId) return;
      const newEls = [];
      for (const msg of data.history || []) {
        if (msg.role !== 'user' && msg.role !== 'assistant') continue;
        const els = _renderHistoryMessage(msg, _historyPager.modelName);
        if (Array.isArray(els)) newEls.push(...els);
      }
      for (const el of newEls) {
        box.insertBefore(el, anchor || box.firstChild);
      }
      _historyPager.offset = Number(data.offset || nextOffset);
      _historyPager.done = !data.has_more_before;
      if (window.hljs) {
        newEls.forEach(el => el.querySelectorAll('pre code:not(.hljs)').forEach(block => window.hljs.highlightElement(block)));
      }
      const heightDelta = box.scrollHeight - beforeHeight;
      box.scrollTop += heightDelta;
    } catch (e) {
      console.warn('Failed to load older chat history:', e);
    } finally {
      if (_historyPager) _historyPager.loading = false;
    }
  };

  _historyPager.handler = () => {
    if (box.scrollTop <= 90) loadOlder();
  };
  box.addEventListener('scroll', _historyPager.handler, { passive: true });
}

function _getIncognitoIds() {
  try { return JSON.parse(sessionStorage.getItem(_INCOGNITO_SESSIONS_KEY) || '[]'); } catch { return []; }
}
function _markIncognito(sid) {
  const ids = _getIncognitoIds();
  if (!ids.includes(sid)) { ids.push(sid); sessionStorage.setItem(_INCOGNITO_SESSIONS_KEY, JSON.stringify(ids)); }
}
function _isIncognitoSession(sid) { return _getIncognitoIds().includes(sid); }
async function _cleanupIncognitoSessions() {
  const ids = _getIncognitoIds();
  if (ids.length === 0) return;
  // Keep the current active incognito session alive, delete the rest
  const toDelete = ids.filter(sid => sid !== currentSessionId);
  if (toDelete.length === 0) return;
  const keep = ids.filter(sid => sid === currentSessionId);
  sessionStorage.setItem(_INCOGNITO_SESSIONS_KEY, JSON.stringify(keep));
  await Promise.all(toDelete.map(sid =>
    fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' }).catch(() => {})
  ));
}

// Research indicator tracking
const _researchingSessions = new Set();
const _streamingSessions = new Set();   // Background chat streams (not polled against research API)
const _completedSessions = new Set();   // Sessions with completed background streams
let _researchPollTimer = null;

// Session list keyboard navigation state
let _sessionListFocused = false;

/** Clear current session from UI (after delete/archive). */
function _deselectCurrentSession(sid) {
  if (currentSessionId !== sid) return;
  currentSessionId = null;
  uiModule.el('chat-history').innerHTML = '';
  uiModule.el('current-meta').textContent = 'Odysseus Chat';
  Storage.remove('lastSessionId');
  history.replaceState(null, '', window.location.pathname);
  if (window.chatModule && window.chatModule.showWelcomeScreen) {
    window.chatModule.showWelcomeScreen();
  }
  // Reset send button to idle state
  const submitBtn = document.querySelector('.send-btn');
  if (submitBtn) {
    submitBtn.dataset.mode = '';
    delete submitBtn.dataset.phase;
    submitBtn.classList.remove('recording');
  }
  if (window._updateSendBtnIcon) window._updateSendBtnIcon();
}

function _removeSessionFromLocalState(sid) {
  if (!sid) return;
  const id = String(sid);
  sessions = sessions.filter(s => String(s.id) !== id);
  _selectedIds.delete(id);
  try {
    const savedOrder = Storage.get('session-order');
    if (savedOrder) {
      const orderIds = JSON.parse(savedOrder);
      if (Array.isArray(orderIds) && orderIds.some(x => String(x) === id)) {
        Storage.set('session-order', JSON.stringify(orderIds.filter(x => String(x) !== id)));
      }
    }
  } catch (e) {
    console.warn('Failed to prune deleted session order:', e);
  }
  document.querySelectorAll('.list-item[data-session-id]').forEach(el => {
    if (String(el.dataset.sessionId) === id) el.remove();
  });
  _deselectCurrentSession(id);
}

function _normalizeSessionsList(fetched) {
  if (!Array.isArray(fetched)) return [];
  const seen = new Set();
  const unique = [];
  for (const session of fetched) {
    if (!session || session.id == null) continue;
    const id = String(session.id);
    if (seen.has(id)) continue;
    seen.add(id);
    unique.push(session);
  }
  return unique;
}

// Initialize dependencies from app.js (no-op: dependencies now imported directly)
export function initDependencies() {}

// ── Folder state persistence ──
const FOLDER_STATE_KEY = 'odysseus-folder-state';
const FOLDER_ORDER_KEY = 'odysseus-folder-order';

function loadFolderState() {
  return Storage.getJSON(FOLDER_STATE_KEY, {});
}
function saveFolderState(state) {
  Storage.setJSON(FOLDER_STATE_KEY, state);
}
function loadFolderOrder() {
  return Storage.getJSON(FOLDER_ORDER_KEY, []);
}
function saveFolderOrder(order) {
  Storage.setJSON(FOLDER_ORDER_KEY, order);
}

/** Get all unique folder names from current sessions. */
function getFolderNames() {
  const names = new Set();
  sessions.forEach(s => { if (s.folder) names.add(s.folder); });
  return Array.from(names).sort();
}

/** Move a session to a folder via the API. */
async function moveToFolder(sessionId, folderName) {
  const fd = new FormData();
  fd.append('folder', folderName || '');
  await fetch(`${API_BASE}/api/session/${sessionId}`, { method: 'PATCH', body: fd });
  // Update local data
  const s = sessions.find(x => x.id === sessionId);
  if (s) s.folder = folderName || null;
  renderSessionList();
}

/** Build the "Move to folder" submenu for a session dropdown. */
function buildFolderSubmenu(sessionId, currentFolder, dropdown) {
  const folders = getFolderNames();

  const moveItem = document.createElement('div');
  moveItem.className = 'dropdown-item-compact';
  moveItem.style.position = 'relative';
  const _folderIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
  moveItem.innerHTML = '<span class="dropdown-icon">' + _folderIcon + '</span><span>Move to folder</span>';

  const sub = document.createElement('div');
  sub.className = 'dropdown session-folder-submenu';

  // "No folder" option
  const noneOpt = document.createElement('div');
  noneOpt.className = 'dropdown-item-compact';
  if (!currentFolder) noneOpt.style.opacity = '0.5';
  noneOpt.textContent = '(No folder)';
  noneOpt.addEventListener('click', async (e) => {
    e.stopPropagation();
    await moveToFolder(sessionId, '');
    dropdown.style.display = 'none';
    sub.style.display = 'none';
  });
  sub.appendChild(noneOpt);

  // Existing folders
  folders.forEach(f => {
    const opt = document.createElement('div');
    opt.className = 'dropdown-item-compact';
    if (f === currentFolder) opt.style.opacity = '0.5';
    opt.textContent = f;
    opt.addEventListener('click', async (e) => {
      e.stopPropagation();
      await moveToFolder(sessionId, f);
      // Auto-flip to By Folder view so the user can see where the
      // chat went, same as when creating a new folder.
      setSortMode('group');
      dropdown.style.display = 'none';
      sub.style.display = 'none';
    });
    sub.appendChild(opt);
  });

  // "New folder" option
  const newOpt = document.createElement('div');
  newOpt.className = 'dropdown-item-compact';
  newOpt.style.color = 'var(--accent-primary)';
  newOpt.textContent = '+ New Folder';
  newOpt.addEventListener('click', async (e) => {
    e.stopPropagation();
    const name = await styledPrompt('Name this folder:', {
      title: 'New folder',
      placeholder: 'e.g. Work, Research, Drafts',
      confirmText: 'Create',
    });
    if (!name || !name.trim()) return;
    await moveToFolder(sessionId, name.trim());
    // Auto-flip to By Folder view so the user immediately sees the
    // folder they just created — otherwise the new folder disappears
    // into the flat list and looks like the action did nothing.
    setSortMode('group');
    dropdown.style.display = 'none';
    sub.style.display = 'none';
  });
  sub.appendChild(newOpt);

  moveItem.addEventListener('click', (e) => {
    e.stopPropagation();
    if (sub.style.display === 'block') {
      sub.style.display = 'none';
    } else {
      const rect = moveItem.getBoundingClientRect();
      const isMobile = window.innerWidth <= 768;
      sub.style.top = '-9999px';
      sub.style.display = 'block';
      const subRect = sub.getBoundingClientRect();

      if (isMobile) {
        // On mobile: position below the dropdown, centered
        const ddRect = dropdown.getBoundingClientRect();
        sub.style.left = Math.max(8, ddRect.left) + 'px';
        sub.style.width = Math.min(ddRect.width, window.innerWidth - 16) + 'px';
        const topBelow = ddRect.bottom + 4;
        if (topBelow + subRect.height > window.innerHeight) {
          sub.style.top = Math.max(8, ddRect.top - subRect.height - 4) + 'px';
        } else {
          sub.style.top = topBelow + 'px';
        }
      } else {
        // Desktop: to the right
        sub.style.left = rect.right + 2 + 'px';
        sub.style.width = '';
        if (rect.top + subRect.height > window.innerHeight) {
          sub.style.top = Math.max(2, window.innerHeight - subRect.height - 4) + 'px';
        } else {
          sub.style.top = rect.top + 'px';
        }
        // Clamp right edge
        if (rect.right + 2 + subRect.width > window.innerWidth - 8) {
          sub.style.left = Math.max(8, rect.left - subRect.width - 2) + 'px';
        }
      }
    }
  });

  sub.addEventListener('click', (e) => e.stopPropagation());
  document.addEventListener('click', () => { sub.style.display = 'none'; });
  document.body.appendChild(sub);

  return moveItem;
}

/** Create a single session list-item element. */
function createSessionItem(s) {
  const div = document.createElement('div');
  div.className = 'list-item session-item';
  div.setAttribute('role', 'option');
  div.setAttribute('tabindex', '-1');
  div.setAttribute('data-session-id', s.id);
  // Special-session sentinel — true for the legacy OpenClaw row, which
  // skips the normal provider dot / name / action chrome. Was
  // previously detected here but the declaration got removed while
  // leaving the references in place, causing ReferenceError on every
  // session list re-render.
  const isOpenClaw = s.is_openclaw || s.id === 'openclaw';

  // Drag handle
  const handle = document.createElement('span');
  handle.className = 'item-drag-handle';
  handle.textContent = '\u22EE\u22EE';
  handle.title = 'Drag to reorder';
  div.appendChild(handle);

  // Provider dot indicator
  if (!isOpenClaw) {
    const star = document.createElement('span');
    const _logo = providerLogo(s.model);
    if (_logo) {
      star.className = 'session-star provider-logo';
      star.innerHTML = _logo;
      star.style.opacity = '0.4';
    } else {
      star.className = 'session-star';
    }
    div.appendChild(star);
  }

  // Session type icon
  const icon = document.createElement('span');
  const _isFork = s.name && (s.name.startsWith('Fork:') || s.name.startsWith('\u2ADD'));
  const _isGroup = s.name && s.name.startsWith('[GRP]');
  icon.className = 'session-icon' + (s.has_documents ? ' has-docs' : '');
  if (_isGroup) {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
  } else if (_isFork) {
    icon.textContent = '\u2ADD';
    icon.style.fontSize = '14px';
  } else if (s.has_documents) {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
  } else if (s.has_images) {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
  } else if (s.mode === 'agent') {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
  } else if (s.mode === 'research') {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>';
  } else {
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
  }
  // Favorite bookmark replaces session-icon when important
  if (s.is_important && !isOpenClaw) {
    icon.className = 'session-icon session-fav';
    icon.title = 'Unfavorite';
    icon.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>';
    icon.addEventListener('click', async (e) => {
      e.stopPropagation();
      const fd = new FormData();
      fd.append('important', false);
      await fetch(`${API_BASE}/api/session/${s.id}/important`, { method: 'POST', body: fd });
      s.is_important = false;
      uiModule.showToast('Unfavorited');
      renderSessionList();
    });
  }
  div.appendChild(icon);

  const span = document.createElement('span');
  span.className = 'grow';
  let chatTitle = s.name || '';
  if (_isFork) chatTitle = chatTitle.replace(/^Fork:\s*/, '').replace(/^\u2ADD\s*/, '');
  if (_isGroup) chatTitle = chatTitle.replace(/^\[GRP\]\s*/, '');
  let label = chatTitle;
  if (s.model) label += ' · ' + s.model.split('/').pop();
  if (s.archived) label += ' [archived]';
  span.textContent = label;
  span.title = (s.model ? s.model.split('/').pop() + ' · ' : '') + chatTitle;
  span.classList.add('text-ellipsis');

  // Double-click to rename (only when session is already selected)
  if (!isOpenClaw) {
    span.addEventListener('dblclick', (e) => {
      if (currentSessionId !== s.id) return; // must be selected first
      e.stopPropagation();
      const input = document.createElement('input');
      input.type = 'text';
      input.value = s.name || '';
      input.className = 'session-rename-input';
      span.replaceWith(input);
      input.focus();
      input.select();
      const _stopGuard = _guardSidebarDuringRename();
      const commit = async () => {
        const newName = input.value.trim();
        if (newName && newName !== s.name) {
          const fd = new FormData();
          fd.append('name', newName);
          await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'PATCH', body: fd });
          s.name = newName;
          uiModule.showToast('Renamed');
        }
        _forceSidebarOpen();
        renderSessionList();
        _stopGuard();
      };
      input.addEventListener('blur', commit);
      input.addEventListener('keydown', (ev) => {
        if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
        if (ev.key === 'Escape') { input.removeEventListener('blur', commit); _forceSidebarOpen(); renderSessionList(); _stopGuard(); }
      });
    });
  }

  // Clicking anywhere on the row selects the session (except drag handle and menu)
  // On mobile, suppress click if user was scrolling (touchmove detected)
  // Long press on mobile shows context menu
  let _touchMoved = false;
  let _longPressTimer = null;
  let _longPressed = false;
  div.addEventListener('touchstart', (e) => {
    _touchMoved = false;
    _longPressed = false;
    if (window.innerWidth > 768) return;
    _longPressTimer = setTimeout(() => {
      _longPressed = true;
      // Haptic feedback if available
      if (navigator.vibrate) navigator.vibrate(30);
      // Show the session dropdown directly (menu button is hidden on mobile)
      const dd = div._sessionDropdown;
      if (dd) {
        // Close any other open dropdowns
        document.querySelectorAll('.dropdown').forEach(d => { if (d !== dd) d.style.display = 'none'; });
        const rect = div.getBoundingClientRect();
        dd.style.position = 'fixed';
        dd.style.left = rect.left + 'px';
        dd.style.top = (rect.bottom + 4) + 'px';
        dd.style.right = 'auto';
        dd.style.display = 'block';
        dd.style.zIndex = '1000';
        // Clamp to viewport
        requestAnimationFrame(() => {
          const mr = dd.getBoundingClientRect();
          if (mr.bottom > window.innerHeight - 8) dd.style.top = (rect.top - mr.height - 4) + 'px';
          if (mr.right > window.innerWidth - 8) { dd.style.left = 'auto'; dd.style.right = '8px'; }
        });
        // Close on tap outside
        const close = (ev) => { if (!dd.contains(ev.target)) { dd.style.display = 'none'; document.removeEventListener('click', close, true); } };
        setTimeout(() => document.addEventListener('click', close, true), 100);
      }
    }, 500);
  }, { passive: true });
  div.addEventListener('touchmove', () => {
    _touchMoved = true;
    if (_longPressTimer) { clearTimeout(_longPressTimer); _longPressTimer = null; }
  }, { passive: true });
  div.addEventListener('touchend', () => {
    if (_longPressTimer) { clearTimeout(_longPressTimer); _longPressTimer = null; }
  }, { passive: true });
  div.addEventListener('click', (e) => {
    if (e.target.closest('.item-drag-handle') || e.target.closest('.session-fav') || e.target.closest('.hamburger') || e.target.closest('.session-dropdown') || e.target.closest('.session-rename-input') || e.target.closest('.session-select-cb')) return;
    if (_touchMoved || _longPressed) { _touchMoved = false; _longPressed = false; return; }
    // In select mode, toggle dot instead of navigating
    if (_selectMode) {
      const dot = div.querySelector('.session-select-cb');
      if (dot) dot.click();
      return;
    }
    selectSession(s.id);
  });

  // Create a dropdown menu button
  const menuBtn = document.createElement('button');
  menuBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
  menuBtn.title = 'Session actions';
  menuBtn.className = 'hamburger session-menu-btn';

  // Create dropdown menu
  const dropdown = document.createElement('div');
  dropdown.className = 'dropdown session-dropdown session-dropdown-menu';

  // Create menu items
  const _icon = (svg) => `<span class="dropdown-icon">${svg}</span>`;
  const _renameIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>';
  const _archiveIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>';
  const _deleteIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>';
  const _copyIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';

  const renameItem = document.createElement('div');
  renameItem.className = 'dropdown-item-compact';
  renameItem.innerHTML = _icon(_renameIcon) + '<span>Rename</span>';

  const archiveItem = document.createElement('div');
  archiveItem.className = 'dropdown-item-compact';
  archiveItem.innerHTML = _icon(_archiveIcon) + '<span>Archive</span>';

  const deleteItem = document.createElement('div');
  deleteItem.className = 'dropdown-item-compact dropdown-item-danger';
  deleteItem.innerHTML = _icon(_deleteIcon) + '<span>Delete</span><span class="dropdown-shortcut">' + _mod + '+Alt+D</span>';



  dropdown.appendChild(renameItem);

  // Star/Unstar item
  if (!isOpenClaw) {
    const _favIcon = s.is_important
      ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>'
      : '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>';
    const starItem = document.createElement('div');
    starItem.className = 'dropdown-item-compact';
    starItem.innerHTML = _icon(_favIcon) + '<span>' + (s.is_important ? 'Unfavorite' : 'Favorite') + '</span><span class="dropdown-shortcut">' + _mod + '+Alt+F</span>';
    starItem.addEventListener('click', async (e) => {
      e.stopPropagation();
      const newVal = !s.is_important;
      const fd = new FormData();
      fd.append('important', newVal);
      await fetch(`${API_BASE}/api/session/${s.id}/important`, { method: 'POST', body: fd });
      s.is_important = newVal;
      dropdown.style.display = 'none';
      renderSessionList();
    });
    dropdown.appendChild(starItem);
  }

  const copyItem = document.createElement('div');
  copyItem.className = 'dropdown-item-compact';
  copyItem.innerHTML = _icon(_copyIcon) + '<span>Copy Chat</span>';
  copyItem.addEventListener('click', async (e) => {
    e.stopPropagation();
    dropdown.style.display = 'none';
    try {
      const res = await fetch(`${API_BASE}/api/history/${s.id}`);
      const data = await res.json();
      const msgs = data.history || [];
      if (!msgs.length) { uiModule.showToast('No messages to copy'); return; }
      const lines = msgs
        .filter(m => m.role === 'user' || m.role === 'assistant')
        .map(m => {
          const label = m.role === 'user' ? 'You' : 'AI';
          const text = typeof m.content === 'string' ? m.content.trim() : JSON.stringify(m.content);
          return `${label}: ${text}`;
        });
      const text = lines.join('\n\n');
      try {
        await navigator.clipboard.writeText(text);
      } catch (_clipErr) {
        // Fallback for non-secure contexts
        const ta = document.createElement('textarea');
        ta.value = text;
        ta.style.cssText = 'position:fixed;left:-9999px';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        ta.remove();
      }
      uiModule.showToast('Chat copied to clipboard');
    } catch (e) {
      console.error('Copy chat failed:', e);
      uiModule.showError('Failed to copy chat');
    }
  });

  // Rename is already appended above (line 393)

  // "Select" — enter bulk select mode with this session pre-selected
  if (!isOpenClaw) {
    const selectMoreItem = document.createElement('div');
    selectMoreItem.className = 'dropdown-item-compact';
    selectMoreItem.innerHTML = _icon('<span style="font-size:16px;line-height:1;">●</span>') + '<span>Select</span>';
    selectMoreItem.addEventListener('click', (e) => {
      e.stopPropagation();
      dropdown.style.display = 'none';
      _enterSelectMode();
      const dot = div.querySelector('.session-select-cb');
      if (dot) { dot._checked = true; dot.innerHTML = '●'; dot.style.opacity = '1'; dot.style.color = 'var(--accent, var(--red))'; _selectedIds.add(s.id); _updateBulkCount(); }
    });
    // On mobile, "Select" is the primary multi-pick action — put it at the top
    // of the menu. On desktop keep its original position.
    if (window.innerWidth <= 768) {
      dropdown.insertBefore(selectMoreItem, dropdown.firstChild);
    } else {
      dropdown.appendChild(selectMoreItem);
    }
  }

  // Copy & Move to folder
  const folderItem = buildFolderSubmenu(s.id, s.folder, dropdown);
  dropdown.appendChild(copyItem);
  dropdown.appendChild(folderItem);

  // Separator before destructive actions
  const _sep = document.createElement('div');
  _sep.style.cssText = 'height:1px;margin:3px 0;background:color-mix(in srgb,var(--border) 40%,transparent)';
  dropdown.appendChild(_sep);

  dropdown.appendChild(archiveItem);
  dropdown.appendChild(deleteItem);

  // Mobile-only Cancel — explicit close for touch users. CSS hides it on
  // desktop (outside-click already dismisses cleanly there).
  const _cancelIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
  const cancelItem = document.createElement('div');
  cancelItem.className = 'dropdown-item-compact dropdown-cancel-mobile';
  cancelItem.innerHTML = _icon(_cancelIcon) + '<span>Cancel</span>';
  cancelItem.addEventListener('click', (e) => {
    e.stopPropagation();
    dropdown.style.display = 'none';
  });
  dropdown.appendChild(cancelItem);

  // Add event listeners
  menuBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    // Close any other open dropdowns
    document.querySelectorAll('.dropdown').forEach(d => {
      if (d !== dropdown) d.style.display = 'none';
    });
    // Toggle this dropdown
    if (dropdown.style.display === 'block') {
      dropdown.style.display = 'none';
    } else {
      // Position the dropdown using viewport coords
      const rect = menuBtn.getBoundingClientRect();
      dropdown.style.left = '';
      dropdown.style.right = (window.innerWidth - rect.right) + 'px';
      // Show off-screen first to measure height
      dropdown.style.top = '-9999px';
      dropdown.style.display = 'block';
      const ddRect = dropdown.getBoundingClientRect();
      // Flip above if not enough room below
      if (rect.bottom + 2 + ddRect.height > window.innerHeight) {
        dropdown.style.top = Math.max(2, rect.top - ddRect.height - 2) + 'px';
      } else {
        dropdown.style.top = rect.bottom + 2 + 'px';
      }
    }
  });

  renameItem.addEventListener('click', () => {
    dropdown.style.display = 'none';
    _forceSidebarOpen();
    // Find the session row's name span and start inline editing
    const sessionEl = document.querySelector(`.list-item[data-session-id="${s.id}"]`);
    if (!sessionEl) return;
    const span = sessionEl.querySelector('.grow');
    if (!span || sessionEl.querySelector('.session-rename-input')) return;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = s.name || '';
    input.className = 'session-rename-input';
    span.replaceWith(input);
    input.focus();
    input.select();
    const _stopGuard = _guardSidebarDuringRename();
    const commit = async () => {
      const newName = input.value.trim();
      if (newName && newName !== s.name) {
        const fd = new FormData();
        fd.append('name', newName);
        await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'PATCH', body: fd });
        s.name = newName;
        uiModule.showToast('Renamed');
      }
      _forceSidebarOpen();
      renderSessionList();
      _stopGuard();
    };
    input.addEventListener('blur', commit);
    input.addEventListener('keydown', (ev) => {
      if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
      if (ev.key === 'Escape') { input.removeEventListener('blur', commit); _forceSidebarOpen(); renderSessionList(); _stopGuard(); }
    });
  });

  deleteItem.addEventListener('click', async () => {
    if (s.is_important) {
      uiModule.showToast('Unfavorite before deleting');
      dropdown.style.display = 'none';
      return;
    }
    dropdown.style.display = 'none';
    if (!await uiModule.styledConfirm('Delete this session?', { confirmText: 'Delete', danger: true })) {
      _forceSidebarOpen();
      return;
    }
    const wasCurrentSession = currentSessionId === s.id;
    // If streaming, abort it before deleting
    if (wasCurrentSession && window.chatModule && window.chatModule.abortCurrentRequest) {
      window.chatModule.abortCurrentRequest();
    }
    _deselectCurrentSession(s.id);
    _removeSessionFromLocalState(s.id);
    _skipAutoSelect = true;
    // Clean up persistent chat mapping
    try {
      const pm = await import('./presets.js');
      if (pm.removePersistentChat) pm.removePersistentChat(s.id);
    } catch (e) {}
    // On mobile, close sidebar if we deleted the active session so user sees welcome screen
    if (wasCurrentSession && window.innerWidth <= 768) {
      const sidebar = document.getElementById('sidebar');
      if (sidebar) sidebar.classList.add('hidden');
      const backdrop = document.getElementById('sidebar-backdrop');
      if (backdrop) backdrop.classList.remove('visible');
    } else {
      _forceSidebarOpen();
    }
    // Await API deletion, then reload the authoritative list from the server
    try {
      await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' });
    } catch (e) { /* network error — session may still exist server-side */ }
    await loadSessions();
  });

  archiveItem.addEventListener('click', async () => {
    dropdown.style.display = 'none';
    _forceSidebarOpen();
    try {
      const response = await fetch(`${API_BASE}/api/session/${s.id}/archive`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' }
      });
      if (response.ok) {
        _forceSidebarOpen();
        await loadSessions();
        dropdown.style.display = 'none';
        uiModule.showToast('Session archived');
      } else {
        throw new Error('Failed to archive session');
      }
    } catch (error) {
      console.error('Error archiving session:', error);
      uiModule.showError('Failed to archive session');
    }
  });

  // Dropdowns are closed by the shared global listener (_initDropdownDismiss)

  // Prevent dropdown from closing when clicking inside it
  dropdown.addEventListener('click', (e) => {
    e.stopPropagation();
  });

  div.appendChild(span);

  // Apply processing/completed state to the star dot
  var _isProcessing = _researchingSessions.has(s.id) || _streamingSessions.has(s.id);
  var _isDone = _completedSessions.has(s.id) && !_isProcessing;
  if (!isOpenClaw) {
    var _starEl = div.querySelector('.session-star');
    if (_starEl) {
      _starEl.dataset.sessionId = s.id;
      if (_isProcessing) {
        _starEl.classList.add('processing');
        _starEl.style.opacity = '1';
      } else if (_isDone) {
        _starEl.classList.add('notify');
        _starEl.style.opacity = '1';
        div.classList.add('stream-complete');
      }
    }
  }

  div.appendChild(menuBtn);
  dropdown.addEventListener('click', (e) => e.stopPropagation());
  document.body.appendChild(dropdown);
  div._sessionDropdown = dropdown;

  return div;
}

function _dateBucketLabel(value) {
  if (!value) return 'Older';
  const d = new Date(value);
  if (Number.isNaN(d.getTime())) return 'Older';
  const dayStart = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
  const today = dayStart(new Date());
  const day = dayStart(d);
  const diff = Math.round((today - day) / 86400000);
  if (diff === 0) return 'Today';
  if (diff === 1) return 'Yesterday';
  if (diff > 1 && diff < 7) return d.toLocaleDateString([], { weekday: 'long' });
  if (diff >= 365) {
    const years = Math.floor(diff / 365);
    return `${years} ${years === 1 ? 'year' : 'years'} ago`;
  }
  if (diff >= 180) return '6 months ago';
  if (diff >= 30) return `${Math.floor(diff / 30) * 30} days ago`;
  const sameYear = d.getFullYear() === new Date().getFullYear();
  return d.toLocaleDateString([], sameYear ? { month: 'long', day: 'numeric' } : { month: 'long', day: 'numeric', year: 'numeric' });
}

function _sessionBucketDate(s) {
  return s.last_message_at || s.updated_at || s.created_at || '';
}

function _loadDateSectionCollapseState() {
  const raw = Storage.getJSON(DATE_SECTION_COLLAPSE_KEY, {});
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
  return raw;
}

function _saveDateSectionCollapseState(state) {
  Storage.setJSON(DATE_SECTION_COLLAPSE_KEY, state && typeof state === 'object' ? state : {});
}

function _dateSectionKey(kind, label) {
  return `${kind || 'session'}:${label || 'Older'}`;
}

function _isDateSectionCollapsed(kind, label) {
  return _loadDateSectionCollapseState()[_dateSectionKey(kind, label)] === true;
}

function _toggleDateSection(kind, label) {
  const state = _loadDateSectionCollapseState();
  const key = _dateSectionKey(kind, label);
  state[key] = state[key] !== true;
  _saveDateSectionCollapseState(state);
  renderSessionList();
}

function _createDateSectionHeader(label, kind = 'session') {
  const el = document.createElement('div');
  el.className = `date-section-header ${kind}-date-section-header`;
  el.textContent = label;
  const collapsed = _isDateSectionCollapsed(kind, label);
  if (collapsed) el.classList.add('collapsed');
  el.dataset.dateSectionKind = kind;
  el.dataset.dateSectionLabel = label;
  el.tabIndex = 0;
  el.setAttribute('role', 'button');
  el.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
  el.title = collapsed ? `Show ${label}` : `Hide ${label}`;
  el.addEventListener('click', (e) => {
    e.stopPropagation();
    _toggleDateSection(kind, label);
  });
  el.addEventListener('keydown', (e) => {
    if (e.key !== 'Enter' && e.key !== ' ') return;
    e.preventDefault();
    e.stopPropagation();
    _toggleDateSection(kind, label);
  });
  return el;
}

function _appendSessionItemsWithDateHeaders(frag, items) {
  let lastLabel = null;
  let collapsed = false;
  for (const s of items) {
    const label = _dateBucketLabel(_sessionBucketDate(s));
    if (label !== lastLabel) {
      frag.appendChild(_createDateSectionHeader(label, 'session'));
      collapsed = _isDateSectionCollapsed('session', label);
      lastLabel = label;
    }
    if (collapsed) continue;
    frag.appendChild(createSessionItem(s));
  }
}

function _appendFavoriteSessionItems(frag, items) {
  if (!items.length) return;
  frag.appendChild(_createDateSectionHeader('Favorites', 'session'));
  if (_isDateSectionCollapsed('session', 'Favorites')) return;
  for (const s of items) {
    frag.appendChild(createSessionItem(s));
  }
}

let _renderRAF = null;
export function renderSessionList() {
  // Debounce rapid re-renders within the same frame
  if (_renderRAF) cancelAnimationFrame(_renderRAF);
  _renderRAF = requestAnimationFrame(_renderSessionListImpl);
}

function _renderSessionListImpl() {
  _renderRAF = null;
  const list = uiModule.el('session-list');
  if (!list) return;

  // Get saved order from localStorage
  const savedOrder = Storage.get('session-order');
  let orderedSessions = sessions.filter(s => !s.archived && s.folder !== 'Assistant' && !_isIncognitoSession(s.id) && (s.name || '').trim() !== 'Nobody' && (s.name || '').trim() !== 'Incognito');

  if (savedOrder) {
    try {
      const orderIds = JSON.parse(savedOrder);
      const sessionMap = new Map(orderedSessions.map(s => [s.id, s]));
      const ordered = [];
      orderIds.forEach(id => {
        if (sessionMap.has(id)) {
          ordered.push(sessionMap.get(id));
          sessionMap.delete(id);
        }
      });
      // Append any new sessions not in saved order
      sessionMap.forEach(s => ordered.push(s));
      orderedSessions = ordered;
    } catch (e) {
      console.warn('Failed to restore session order:', e);
    }
  }

  // Clean up any previous session dropdowns and folder submenus from body
  document.querySelectorAll('.session-dropdown, .folder-submenu').forEach(d => d.remove());

  const _frag = document.createDocumentFragment();

  // ── Flat sort modes: ignore folders, show one ordered list. ──
  // Folders are only shown when _sortMode === 'group' (or null/empty
  // for manual mode). This keeps the picker simple: a folder-grouped
  // view is one of the sort choices, alongside Last Active / Newest.
  if (_sortMode && _sortMode !== 'group') {
    orderedSessions.sort((a, b) => {
      if (_sortMode === 'newest') return (b.created_at || '').localeCompare(a.created_at || '');
      // "Last active" sorts by the last actual MESSAGE, not updated_at —
      // updated_at is bumped by renames / model swaps / folder moves, which
      // made the order feel random. Fall back to updated_at/created_at for
      // older rows that predate the last_message_at backfill.
      if (_sortMode === 'active') {
        const av = a.last_message_at || a.updated_at || a.created_at || '';
        const bv = b.last_message_at || b.updated_at || b.created_at || '';
        return bv.localeCompare(av);
      }
      return 0;
    });
    // Favorites are a global pinned block above date buckets, not just
    // promoted within the day they belong to.
    const allFlat = [
      ...orderedSessions.filter(s => s.is_important),
      ...orderedSessions.filter(s => !s.is_important),
    ];

    const limit = _showAllSessions ? allFlat.length : SIDEBAR_MAX_VISIBLE;
    const visible = allFlat.slice(0, limit);
    const activeIdx = allFlat.findIndex(s => s.id === currentSessionId);
    if (!_showAllSessions && activeIdx >= limit) visible.push(allFlat[activeIdx]);

    const visibleFavorites = visible.filter(s => s.is_important);
    const visibleRegular = visible.filter(s => !s.is_important);
    _appendFavoriteSessionItems(_frag, visibleFavorites);
    _appendSessionItemsWithDateHeaders(_frag, visibleRegular);

    if (allFlat.length > SIDEBAR_MAX_VISIBLE) {
      const remaining = allFlat.length - SIDEBAR_MAX_VISIBLE;
      const toggleBtn = document.createElement('button');
      toggleBtn.className = 'session-show-more-btn';
      toggleBtn.textContent = _showAllSessions ? 'Show less' : `Show ${remaining} more`;
      toggleBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        _showAllSessions = !_showAllSessions;
        renderSessionList();
      });
      _frag.appendChild(toggleBtn);
    }

    list.innerHTML = '';
    list.appendChild(_frag);
    _postRenderSessionList(list);
    return;
  }

  // ── Group / manual mode: render folders, then unfiled sessions. ──
  const folderState = loadFolderState();
  const folders = {}; // folderName -> [sessions]
  const unfiled = [];

  orderedSessions.forEach(s => {
    if (s.folder) {
      if (!folders[s.folder]) folders[s.folder] = [];
      folders[s.folder].push(s);
    } else {
      unfiled.push(s);
    }
  });

  // Move starred sessions to top of each group, preserving relative order
  const starPartition = (arr) => {
    const starred = arr.filter(s => s.is_important);
    const rest = arr.filter(s => !s.is_important);
    arr.length = 0;
    arr.push(...starred, ...rest);
  };
  starPartition(unfiled);
  Object.values(folders).forEach(arr => starPartition(arr));

  // Render folders first (above unfiled sessions)
  const savedFolderOrder = loadFolderOrder();
  const allFolderNames = Object.keys(folders);
  const orderedFolderNames = [];
  savedFolderOrder.forEach(name => {
    if (allFolderNames.includes(name)) orderedFolderNames.push(name);
  });
  allFolderNames.forEach(name => {
    if (!orderedFolderNames.includes(name)) orderedFolderNames.push(name);
  });

  orderedFolderNames.forEach(folderName => {
    const folderDiv = document.createElement('div');
    folderDiv.className = 'session-folder';
    folderDiv.dataset.folderName = folderName;

    const header = document.createElement('div');
    header.className = 'session-folder-header';
    header.dataset.folderName = folderName;
    const collapsed = folderState[folderName] === false;

    // Drag handle for folder reordering
    const dragHandle = document.createElement('span');
    dragHandle.className = 'folder-drag-handle';
    dragHandle.textContent = '\u2630';
    dragHandle.title = 'Drag to reorder folder';
    header.appendChild(dragHandle);

    const toggle = document.createElement('span');
    toggle.className = 'folder-toggle';
    toggle.textContent = collapsed ? '\u25B6' : '\u25BC';
    header.appendChild(toggle);

    const nameSpan = document.createElement('span');
    nameSpan.className = 'folder-name';
    nameSpan.textContent = folderName;
    header.appendChild(nameSpan);

    const countSpan = document.createElement('span');
    countSpan.className = 'folder-count';
    countSpan.textContent = `(${folders[folderName].length})`;
    header.appendChild(countSpan);

    // Delete folder button
    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'folder-delete-btn';
    deleteBtn.textContent = '\u00d7';
    deleteBtn.title = 'Delete folder and all sessions';
    deleteBtn.addEventListener('click', async (e) => {
      e.stopPropagation();
      const count = folders[folderName].length;
      if (!await uiModule.styledConfirm(`Delete folder "${folderName}" and all ${count} session(s) inside it?`, { confirmText: 'Delete', danger: true })) return;
      for (const s of folders[folderName]) {
        try {
          await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' });
          _deselectCurrentSession(s.id);
        } catch (err) {
          console.error('Failed to delete session:', s.id, err);
        }
      }
      await loadSessions();
    });
    header.appendChild(deleteBtn);

    let _folderTouchMoved = false;
    header.addEventListener('touchstart', () => { _folderTouchMoved = false; }, { passive: true });
    header.addEventListener('touchmove', () => { _folderTouchMoved = true; }, { passive: true });
    header.addEventListener('click', (e) => {
      e.stopPropagation();
      if (e.target.closest('.folder-drag-handle') || e.target.closest('.folder-delete-btn')) return;
      if (_folderTouchMoved) { _folderTouchMoved = false; return; }
      const state = loadFolderState();
      const isCollapsed = state[folderName] === false;
      state[folderName] = isCollapsed ? true : false;
      saveFolderState(state);
      renderSessionList();
    });

    // Allow renaming folder via double-click
    header.addEventListener('dblclick', async (e) => {
      e.stopPropagation();
      if (e.target.closest('.folder-delete-btn')) return;
      const newName = await styledPrompt('Rename folder:', {
        title: 'Rename folder',
        defaultValue: folderName,
        confirmText: 'Rename',
      });
      if (!newName || !newName.trim() || newName.trim() === folderName) return;
      const promises = folders[folderName].map(s => moveToFolder(s.id, newName.trim()));
      Promise.all(promises).then(() => loadSessions());
    });

    folderDiv.appendChild(header);

    if (!collapsed) {
      const content = document.createElement('div');
      content.className = 'session-folder-content';
      const folderSessions = folders[folderName];
      const folderExpanded = _expandedFolders[folderName];
      const folderLimit = folderExpanded ? folderSessions.length : FOLDER_MAX_VISIBLE;
      const visibleFolder = folderSessions.slice(0, folderLimit);

      // Always include active session even if beyond limit
      const activeInFolder = folderSessions.findIndex(s => s.id === currentSessionId);
      if (!folderExpanded && activeInFolder >= folderLimit) {
        visibleFolder.push(folderSessions[activeInFolder]);
      }

      _appendSessionItemsWithDateHeaders(content, visibleFolder);

      if (folderSessions.length > FOLDER_MAX_VISIBLE) {
        const rem = folderSessions.length - FOLDER_MAX_VISIBLE;
        const moreBtn = document.createElement('button');
        moreBtn.className = 'session-show-more-btn';
        moreBtn.textContent = folderExpanded ? 'Show less' : `Show ${rem} more`;
        moreBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          _expandedFolders[folderName] = !folderExpanded;
          renderSessionList();
        });
        content.appendChild(moreBtn);
      }

      folderDiv.appendChild(content);
    }

    _frag.appendChild(folderDiv);
  });

  // Render unfiled sessions below folders (capped unless expanded)
  const hasFolders = orderedFolderNames.length > 0;
  const activeInUnfiled = unfiled.findIndex(s => s.id === currentSessionId);
  const limit = _showAllSessions ? unfiled.length : SIDEBAR_MAX_VISIBLE;
  const visibleUnfiled = unfiled.slice(0, limit);

  // If active session is beyond the limit, include it
  if (!_showAllSessions && activeInUnfiled >= limit) {
    visibleUnfiled.push(unfiled[activeInUnfiled]);
  }

  // Wrap in "Unsorted" folder if real folders exist
  let unfiledTarget = _frag;
  if (hasFolders && unfiled.length > 0) {
    const unsortedDiv = document.createElement('div');
    unsortedDiv.className = 'session-folder unsorted-folder';
    const unsortedHeader = document.createElement('div');
    unsortedHeader.className = 'session-folder-header';
    const unsortedCollapsed = loadFolderState()['__unsorted__'] === false;

    const dragHandle = document.createElement('span');
    dragHandle.className = 'folder-drag-handle';
    dragHandle.textContent = '\u2630';
    dragHandle.title = 'Drag to reorder folder';
    unsortedHeader.appendChild(dragHandle);

    const toggle = document.createElement('span');
    toggle.className = 'folder-toggle';
    toggle.textContent = unsortedCollapsed ? '\u25B6' : '\u25BC';
    unsortedHeader.appendChild(toggle);
    const nameSpan = document.createElement('span');
    nameSpan.className = 'folder-name';
    nameSpan.textContent = 'Unsorted';
    unsortedHeader.appendChild(nameSpan);
    const countSpan = document.createElement('span');
    countSpan.className = 'folder-count';
    countSpan.textContent = `(${unfiled.length})`;
    unsortedHeader.appendChild(countSpan);

    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'folder-delete-btn';
    deleteBtn.textContent = '\u00d7';
    deleteBtn.title = 'Delete all unsorted sessions';
    deleteBtn.addEventListener('click', async (e) => {
      e.stopPropagation();
      if (!await uiModule.styledConfirm(`Delete all ${unfiled.length} unsorted session(s)?`, { confirmText: 'Delete', danger: true })) return;
      for (const s of unfiled) {
        try {
          await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' });
          _deselectCurrentSession(s.id);
        } catch (err) {
          console.error('Failed to delete session:', s.id, err);
        }
      }
      await loadSessions();
    });
    unsortedHeader.appendChild(deleteBtn);

    unsortedHeader.addEventListener('click', (e) => {
      e.stopPropagation();
      const state = loadFolderState();
      state['__unsorted__'] = state['__unsorted__'] === false ? true : false;
      saveFolderState(state);
      renderSessionList();
    });
    unsortedDiv.appendChild(unsortedHeader);
    if (!unsortedCollapsed) {
      const content = document.createElement('div');
      content.className = 'session-folder-content';
      unfiledTarget = content;
      unsortedDiv.appendChild(content);
    }
    _frag.appendChild(unsortedDiv);
    if (unsortedCollapsed) {
      unfiledTarget = null;
    }
  }

  if (unfiledTarget) {
    _appendSessionItemsWithDateHeaders(unfiledTarget, visibleUnfiled);
  }

  // "Show more" / "Show less" toggle
  if (unfiledTarget && unfiled.length > SIDEBAR_MAX_VISIBLE) {
    const remaining = unfiled.length - SIDEBAR_MAX_VISIBLE;
    const toggleBtn = document.createElement('button');
    toggleBtn.className = 'session-show-more-btn';
    toggleBtn.textContent = _showAllSessions ? 'Show less' : `Show ${remaining} more`;
    toggleBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      _showAllSessions = !_showAllSessions;
      renderSessionList();
    });
    unfiledTarget.appendChild(toggleBtn);
  }

  // Flush all built elements into the list in one operation
  list.innerHTML = '';
  list.appendChild(_frag);

  _postRenderSessionList(list);
}

/** Shared post-render: highlight, keyboard nav, swipe hint, drag sort */
function _postRenderSessionList(list) {
  if (currentSessionId) {
    const activeEl = document.querySelector(`.list-item[data-session-id="${currentSessionId}"]`);
    if (activeEl) {
      activeEl.classList.add('active-session');
      if (_sessionListFocused) activeEl.focus();
    }
  }

  _initKeyboardNav(list);
  _initSwipeToDelete(list);
  initDragSort();
  _showSwipeHint(list);
}

function _initKeyboardNav(list) {
  if (!list._kbInit) {
    list._kbInit = true;
    list.addEventListener('keydown', _onSessionListKeydown);
    list.addEventListener('focusin', () => { _sessionListFocused = true; });
    list.addEventListener('focusout', (e) => {
      if (!list.contains(e.relatedTarget)) _sessionListFocused = false;
    });
  }
}

function _initSwipeToDelete(list) {
  // handled by existing swipe code — placeholder for consistency
}

function _showSwipeHint(list) {
  if ('ontouchstart' in window && !localStorage.getItem('ody-swipe-hint-shown')) {
    const firstItem = list.querySelector('.session-item');
    if (firstItem) {
      localStorage.setItem('ody-swipe-hint-shown', '1');
      const hint = document.createElement('div');
      hint.className = 'swipe-hint';
      hint.innerHTML = '<span class="swipe-hint-arrow">\u2190</span> swipe to delete';
      firstItem.style.position = 'relative';
      firstItem.appendChild(hint);
      setTimeout(() => { hint.style.opacity = '0'; }, 3000);
      setTimeout(() => { hint.remove(); }, 3500);
    }
  }
}

// ── Force sidebar open on mobile (after dropdown actions) ──
function _forceSidebarOpen() {
  if (window.innerWidth > 768) return;
  // Suppress backdrop close
  if (window._suppressSidebarClose !== undefined) {
    window._suppressSidebarClose = true;
    setTimeout(() => { window._suppressSidebarClose = false; }, 2000);
  }
  // Force sidebar visible
  requestAnimationFrame(() => {
    const sb = document.getElementById('sidebar');
    if (sb && sb.classList.contains('hidden')) {
      sb.classList.remove('hidden');
      if (window.syncRailSide) window.syncRailSide();
    }
  });
}

// While an inline rename is in progress on mobile, several paths can hide the
// sidebar (backdrop tap, soft-keyboard viewport resize, dropdown dismiss). Watch
// the sidebar directly and re-open it if anything hides it — bulletproof against
// whichever path fires. Returns a stopper to call once the rename is committed.
function _guardSidebarDuringRename() {
  if (window.innerWidth > 768 || !window.MutationObserver) return () => {};
  const sb = document.getElementById('sidebar');
  if (!sb) return () => {};
  const obs = new MutationObserver(() => {
    if (sb.classList.contains('hidden')) {
      sb.classList.remove('hidden');
      const bd = document.getElementById('sidebar-backdrop');
      if (bd) bd.classList.add('visible');
    }
  });
  obs.observe(sb, { attributes: true, attributeFilter: ['class'] });
  // Keep guarding briefly after the caller stops, to catch the keyboard-dismiss
  // resize that fires just after blur/commit.
  return () => setTimeout(() => obs.disconnect(), 400);
}

// ── Bulk select mode ──
let _selectMode = false;
let _selectedIds = new Set();

function _enterSelectMode() {
  _selectMode = true;
  _selectedIds.clear();
  const bulkBar = document.getElementById('session-bulk-bar');
  if (bulkBar) bulkBar.classList.remove('hidden');
  const selectBtn = document.getElementById('session-select-btn');
  if (selectBtn) selectBtn.style.opacity = '1';
  // Add select dots to all session items
  document.querySelectorAll('.list-item[data-session-id]').forEach(item => {
    if (item.querySelector('.session-select-cb')) return;
    const dot = document.createElement('span');
    dot.className = 'session-select-cb';
    dot.innerHTML = '○';
    dot.style.cssText = 'cursor:pointer;font-size:16px;flex-shrink:0;opacity:0.4;transition:opacity 0.1s;user-select:none;';
    dot._checked = false;
    dot.addEventListener('click', (e) => {
      e.stopPropagation();
      dot._checked = !dot._checked;
      dot.innerHTML = dot._checked ? '●' : '○';
      dot.style.opacity = dot._checked ? '1' : '0.4';
      dot.style.color = dot._checked ? 'var(--accent, var(--red))' : '';
      const sid = item.dataset.sessionId;
      if (dot._checked) _selectedIds.add(sid);
      else _selectedIds.delete(sid);
      _updateBulkCount();
    });
    item.insertBefore(dot, item.firstChild);
  });
  _updateBulkCount();
}

function _exitSelectMode() {
  _selectMode = false;
  _selectedIds.clear();
  const bulkBar = document.getElementById('session-bulk-bar');
  if (bulkBar) bulkBar.classList.add('hidden');
  const selectBtn = document.getElementById('session-select-btn');
  if (selectBtn) selectBtn.style.opacity = '0.5';
  const selectAll = document.getElementById('session-select-all');
  if (selectAll) selectAll.checked = false;
  // Remove checkboxes
  document.querySelectorAll('.session-select-cb').forEach(cb => cb.remove());
}

function _updateBulkCount() {
  const count = _selectedIds.size;
  const archiveBtn = document.getElementById('session-bulk-archive');
  const deleteBtn = document.getElementById('session-bulk-delete');
  if (archiveBtn) { archiveBtn.disabled = count === 0; archiveBtn.style.opacity = count === 0 ? '0.2' : ''; }
  if (deleteBtn) { deleteBtn.disabled = count === 0; deleteBtn.style.opacity = count === 0 ? '0.2' : ''; }
}

function _initBulkSelect() {
  const selectBtn = document.getElementById('session-select-btn');
  if (selectBtn) {
    selectBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      if (_selectMode) _exitSelectMode();
      else _enterSelectMode();
    });
  }
  const cancelBtn = document.getElementById('session-bulk-cancel');
  if (cancelBtn) cancelBtn.addEventListener('click', () => _exitSelectMode());

  // Select from funnel dropdown
  const selectFromDropdown = document.getElementById('session-select-from-dropdown');
  if (selectFromDropdown) {
    selectFromDropdown.addEventListener('click', () => {
      const dd = document.getElementById('session-sort-dropdown');
      if (dd) dd.style.display = 'none';
      _enterSelectMode();
    });
  }

  // Escape exits select mode
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && _selectMode) {
      _exitSelectMode();
    }
  });

  const selectAll = document.getElementById('session-select-all');
  const selectAllDot = document.getElementById('session-select-all-dot');
  const selectAllLabel = document.getElementById('session-select-all-label');
  if (selectAll && selectAllDot) {
    const _toggleAll = () => {
      selectAll.checked = !selectAll.checked;
      selectAllDot.innerHTML = selectAll.checked ? '●' : '○';
      selectAllDot.style.opacity = selectAll.checked ? '1' : '0.4';
      selectAllDot.style.color = selectAll.checked ? 'var(--accent, var(--red))' : '';
      document.querySelectorAll('.session-select-cb').forEach(dot => {
        dot._checked = selectAll.checked;
        dot.innerHTML = selectAll.checked ? '●' : '○';
        dot.style.opacity = selectAll.checked ? '1' : '0.4';
        dot.style.color = selectAll.checked ? 'var(--accent, var(--red))' : '';
        const sid = dot.closest('[data-session-id]')?.dataset.sessionId;
        if (sid) {
          if (selectAll.checked) _selectedIds.add(sid);
          else _selectedIds.delete(sid);
        }
      });
      _updateBulkCount();
    };
    selectAllDot.addEventListener('click', _toggleAll);
    if (selectAllLabel) selectAllLabel.addEventListener('click', _toggleAll);
  }

  const archiveBtn = document.getElementById('session-bulk-archive');
  if (archiveBtn) {
    archiveBtn.addEventListener('click', async () => {
      if (_selectedIds.size === 0) return;
      const count = _selectedIds.size;
      if (!await uiModule.styledConfirm(`Archive ${count} session(s)?`, { confirmText: 'Archive' })) return;
      for (const sid of _selectedIds) {
        try {
          await fetch(`${API_BASE}/api/session/${sid}/archive`, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
        } catch (_) {}
      }
      _exitSelectMode();
      if (window._suppressSidebarClose !== undefined) { window._suppressSidebarClose = true; setTimeout(() => { window._suppressSidebarClose = false; }, 1500); }
      await loadSessions();
      uiModule.showToast(`${count} session(s) archived`);
    });
  }

  const deleteBtn = document.getElementById('session-bulk-delete');
  if (deleteBtn) {
    deleteBtn.addEventListener('click', async () => {
      if (_selectedIds.size === 0) return;
      const count = _selectedIds.size;
      if (!await uiModule.styledConfirm(`Delete ${count} session(s)? This cannot be undone.`, { confirmText: 'Delete', danger: true })) return;
      const deletedIds = [];
      for (const sid of _selectedIds) {
        try {
          const res = await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
          if (res.ok) deletedIds.push(sid);
        } catch (_) {}
      }
      await _animateSessionRowsRemoving(deletedIds, '#session-list .list-item[data-session-id]');
      _exitSelectMode();
      if (window._suppressSidebarClose !== undefined) { window._suppressSidebarClose = true; setTimeout(() => { window._suppressSidebarClose = false; }, 1500); }
      await loadSessions();
      uiModule.showToast(`${deletedIds.length} session(s) deleted`);
    });
  }
}

function _animateSessionRowsRemoving(ids, selector) {
  const idSet = new Set((ids || []).map(id => String(id)));
  if (!idSet.size) return Promise.resolve();
  const rows = Array.from(document.querySelectorAll(selector || '.list-item[data-session-id]'))
    .filter(row => idSet.has(String(row.dataset.sessionId || row.dataset.sid)));
  if (!rows.length) return Promise.resolve();
  for (const row of rows) {
    row.style.maxHeight = `${Math.max(row.getBoundingClientRect().height, row.scrollHeight)}px`;
    row.classList.add('memory-tidy-removing');
  }
  return new Promise(resolve => setTimeout(resolve, 520));
}

export async function loadSessions() {
  try {
    // Delete incognito sessions left over from a previous page load
    await _cleanupIncognitoSessions();

    // Use prefetched data from login page if available (first load only)
    const prefetched = sessionStorage.getItem('ody-prefetch-sessions');
    let fetched;
    if (prefetched) {
      sessionStorage.removeItem('ody-prefetch-sessions');
      fetched = JSON.parse(prefetched);
    } else {
      let url = `${API_BASE}/api/sessions`;
      if (currentSessionId && _isIncognitoSession(currentSessionId)) {
        url += `?active_incognito_id=${encodeURIComponent(currentSessionId)}`;
      }
      const res = await fetch(url);
      fetched = await res.json();
    }
    sessions = _normalizeSessionsList(fetched);
    renderSessionList();

    const sessionsSection = uiModule.el('sessions-section');
    if (sessions.length === 0) {
      sessionsSection.classList.add('hidden');
    } else {
      sessionsSection.classList.remove('hidden');
    }

    const activeSessions = sessions.filter(s => !s.archived);
    // "Transient" sessions = the singleton Assistant chat + any task-output
    // session. Treat them as not-restorable so coming back to the app lands
    // on the user's last actual conversation, not whichever check-in task
    // most recently appended a message.
    const _isTransient = (s) => !!s && (s.folder === 'Assistant' || s.folder === 'Tasks');
    const _realSessions = activeSessions.filter(s => !_isTransient(s));
    let hashId = window.location.hash.replace('#', '');
    if (/^(document|note|image|email|event|task|skill|research)-/.test(hashId) || /^open=notes&note=/.test(hashId)) {
      hashId = '';
    }
    const _isFirstLoad = !sessionStorage.getItem('ody-session-active');
    const _freshRootLoad = !_rootFreshChatApplied && !hashId && !currentSessionId && !_pendingChat;
    if (_freshRootLoad) {
      _rootFreshChatApplied = true;
      Storage.remove('lastSessionId');
    }
    let savedId = _freshRootLoad ? null : Storage.get('lastSessionId');
    // If the persisted lastSessionId points to a transient session (legacy
    // state from before the persistence-guard was added), drop it.
    if (savedId) {
      const _saved = activeSessions.find(s => s.id === savedId);
      if (_saved && _isTransient(_saved)) {
        Storage.remove('lastSessionId');
        savedId = null;
      }
    }
    const hasPendingChat = !!_pendingChat;
    let targetId = null;
    if (hasPendingChat) {
      // A model was picked and the UI is showing a fresh New Chat, but the
      // session is not created until the first message. Background stream
      // completions call loadSessions() later; without this guard that reload
      // sees no current session and auto-selects the previous chat.
      targetId = null;
    } else if (hashId && activeSessions.some(s => s.id === hashId)) {
      targetId = hashId;
    } else if (currentSessionId && activeSessions.some(s => s.id === currentSessionId)) {
      targetId = currentSessionId;
    } else if (currentSessionId) {
      // Session was just created but may not be in the list yet — keep it
      targetId = currentSessionId;
    } else if (!_freshRootLoad && savedId && activeSessions.some(s => s.id === savedId)) {
      targetId = savedId;
    } else if (!_freshRootLoad && !_skipAutoSelect && _realSessions.length > 0) {
      // Most-recent NON-transient session — skip Assistant / Tasks so the
      // auto-firing assistant doesn't become the apparent default chat.
      targetId = _realSessions[0].id;
    } else if (!_freshRootLoad && !_skipAutoSelect && activeSessions.length > 0) {
      // Only transient sessions exist (brand-new account) — fall through to
      // the original behaviour so we don't leave the user with nothing.
      targetId = activeSessions[0].id;
    }
    _skipAutoSelect = false;

    // Fresh login: prefer a default-model session so a brand-new user lands
    // ready to chat. CRITICAL: only do this when there's NO session to return
    // to (no hash / lastSessionId / existing chat resolved into targetId).
    // Otherwise a fresh page load — which a server restart triggers — would
    // spin up a new empty default-model chat and shadow the user's last
    // conversation, making it look like the chat "lost its context" (and the
    // picker would still show the old model's name from cached state). See
    // the targetId resolution above (hash → currentSession → lastSessionId →
    // most-recent).
    if (_isFirstLoad) sessionStorage.setItem('ody-session-active', '1');
    if ((_isFirstLoad || _freshRootLoad) && !targetId) {
      // Land on a visually fresh chat without creating hidden pending session
      // state. The send path can create the default-backed session when the
      // user actually submits. Pre-creating here races with opening an existing
      // chat and was causing sends to jump into brand-new chats.
      if (window.innerWidth < 768) {
        const sb = document.getElementById('sidebar');
        if (sb) sb.classList.add('hidden');
      }
    }

    const suppressSessionLoading = _suppressNextSessionLoading;
    _suppressNextSessionLoading = false;

    if (targetId && targetId !== currentSessionId) {
      const showLoading = !suppressSessionLoading && !(_isFirstLoad && !hashId);
      await selectSession(targetId, { keepSidebar: true, showLoading });
    } else if (targetId && targetId === currentSessionId) {
      // Same session — just refresh the header name in case it was auto-generated
      const s = sessions.find(x => x.id === targetId);
      const metaEl = document.getElementById('current-meta');
      if (metaEl && s) metaEl.textContent = s.name;
    }

    // No session selected — still enable input so slash commands (e.g. /setup) work
    if (!targetId && !hasPendingChat) {
      const msgInput = document.getElementById('message');
      if (msgInput) {
        msgInput.disabled = false;
        if (window.innerWidth > 768) msgInput.focus();
      }
      if (window.chatModule && window.chatModule.showWelcomeScreen) {
        window.chatModule.showWelcomeScreen();
      }
      updateModelPicker();
      // Only auto-create if there are truly zero sessions (not just unselected)
      if (activeSessions.length === 0 && !_autoCreateInProgress) {
        _autoCreateInProgress = true;
        try {
          const dc = await _getPreferredDefaultChat();
          if (dc && dc.endpoint_url && dc.model) {
              await createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default' });
          }
        } catch (_) { /* no default model — that's fine, user can /setup */ }
        _autoCreateInProgress = false;
      }
    }
  } catch (error) {
    console.error('Error in loadSessions:', error);
    uiModule.showError('Failed to load sessions: ' + error.message);
  }
}

export async function selectSession(id, { keepSidebar = false, showLoading = true, immediateLoading = false } = {}) {
  // Exit compare mode cleanly if active
  if (window.compareModule && window.compareModule.isActive()) {
    window.compareModule.deactivate(true);
    return; // deactivate does a page reload
  }
  try {
    const navToken = ++_sessionNavToken;
    const prevSessionId = currentSessionId;
    // Selecting a real persisted chat cancels any deferred "New Chat" model
    // pick. Otherwise the next send can materialize that pending chat instead
    // of posting into the session the user just opened.
    if (_pendingChat) {
      _pendingChat = null;
      _pendingMaterializePromise = null;
    }
    _clearHistoryPager();
    // Re-archive peeked session when navigating away
    _checkPeekCleanup(id);
    // Clear any leftover document text selection so it doesn't bleed into the new chat
    if (prevSessionId !== id && window.documentModule?.clearSelection) {
      try { window.documentModule.clearSelection(); } catch {}
    }
    currentSessionId = id;
    try { window.__odysseusLastSelectedSessionId = id; } catch (_) {}
    // Identify Assistant / task-output sessions so we don't "trap" the user
    // there on return. Skipped from both `lastSessionId` persistence and the
    // URL hash — the user complained that coming back to Odysseus kept
    // landing them on the auto-firing task-log chat instead of their last
    // real conversation.
    const _meta = sessions.find(s => s.id === id);
    const _isTransientChat = !!_meta && (_meta.folder === 'Assistant' || _meta.folder === 'Tasks');
    if (!_isTransientChat) {
      Storage.set('lastSessionId', id);
    }
    // Restore character preset for persistent chats
    try {
      const presetsModule = window.presetsModule || (await import('./presets.js')).default;
      if (presetsModule && presetsModule.onSessionSwitch) presetsModule.onSessionSwitch(id);
    } catch (e) {}
    const meta = sessions.find(s => s.id === id);

    // Detach any in-flight stream to background instead of aborting
    try {
      if (window.chatModule) {
        if (window.chatModule.detachCurrentStream) {
          window.chatModule.detachCurrentStream(prevSessionId);
        } else if (window.chatModule.abortCurrentRequest) {
          window.chatModule.abortCurrentRequest();
        }
      }
    } catch (e) {
      console.warn('detachCurrentStream error:', e);
      if (window.chatModule && window.chatModule.abortCurrentRequest) {
        window.chatModule.abortCurrentRequest();
      }
    }
    // Reset send button to idle state
    if (window._updateSendBtnIcon) window._updateSendBtnIcon();
    const sendBtn = document.querySelector('.send-btn');
    if (sendBtn && sendBtn.dataset.mode === 'streaming') {
      sendBtn.dataset.mode = '';
      sendBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
      sendBtn.title = 'Send message';
    }
    // Deactivate compare mode on session switch
    if (window.compareModule) {
      if (window.compareModule.isActive()) window.compareModule.deactivate(true);
      else if (window.compareModule.hasVisibleResults()) window.compareModule.cleanupResults();
    }
    const msgInput = document.getElementById('message');
    if (msgInput) {
      msgInput.disabled = false;
      _clearComposerUnlessStartupTyped(msgInput);
      msgInput.style.height = '';
      msgInput.style.overflow = '';
      autoResize(msgInput);
    }
    const sendBtn2 = document.querySelector('.send-btn');
    if (sendBtn2) {
      sendBtn2.style.color = '';
      if (window._updateSendBtnIcon) window._updateSendBtnIcon();
    }

    // On mobile manual chat switches, move the drawer away before showing the
    // loader so the status sits over the chat pane instead of being hidden by
    // the sidebar. Startup auto-restore passes keepSidebar + showLoading=false.
    if (showLoading && !keepSidebar && window.innerWidth <= 768) {
      const sidebar = document.getElementById('sidebar');
      const backdrop = document.getElementById('sidebar-backdrop');
      if (sidebar) sidebar.classList.add('hidden');
      if (backdrop) backdrop.classList.remove('visible');
    }

    // Highlight active session in sidebar
    document.querySelectorAll('.list-item.active-session').forEach(el => el.classList.remove('active-session'));
    const activeEl = document.querySelector(`.list-item[data-session-id="${id}"]`);
    if (activeEl) activeEl.classList.add('active-session');

    const currentMetaEl = uiModule.el('current-meta');
    if (currentMetaEl) {
      currentMetaEl.textContent = meta ? meta.name : 'Odysseus Chat';
    }
    // Update model picker visibility
    updateModelPicker();
    if (window.refreshChatContextHeader) window.refreshChatContextHeader('select-session');

    // Refresh session cost badge for the newly selected session
    if (chatRenderer.updateSessionCostUI) chatRenderer.updateSessionCostUI();

    const chatHistory = uiModule.el('chat-history');
    // Prefetch history before fading so we can swap instantly. `isOC`
    // is the OpenClaw special-session sentinel — used by the wouldWipe
    // guard below and the welcome-screen branch further down. (Its
    // declaration had been removed while leaving the references in
    // place, producing a ReferenceError every selectSession.)
    const isOC = meta && (meta.is_openclaw || id === 'openclaw');
    let msgHistory = [], modelName = null, pageInfo = null;
    let paintedLoading = false;
    let loadingTimer = null;
    let loadingPaintReady = Promise.resolve();
    if (!isOC) {
      if (showLoading && chatHistory && prevSessionId !== id) {
        const loadingDelayMs = immediateLoading ? 0 : (window.innerWidth <= 768 ? 900 : 500);
        loadingTimer = setTimeout(() => {
          if (navToken !== _sessionNavToken || currentSessionId !== id) return;
          _paintSessionLoading(chatHistory, 'Loading chat');
          paintedLoading = true;
          loadingPaintReady = _nextPaint();
        }, loadingDelayMs);
      }
      const res = await fetch(_historyUrl(id, { limit: _historyPageLimit() }));
      const data = await res.json();
      if (loadingTimer) {
        clearTimeout(loadingTimer);
        loadingTimer = null;
      }
      if (paintedLoading) {
        await loadingPaintReady;
      }
      if (navToken !== _sessionNavToken || currentSessionId !== id) return;
      msgHistory = data.history || [];
      modelName = data.model || null;
      pageInfo = {
        offset: data.offset,
        limit: data.limit,
        total: data.total,
        has_more_before: !!data.has_more_before,
      };
      // The model returned by /api/history is the authoritative one the
      // backend will use for this session. Write it back into the cached
      // session meta and refresh the picker so the displayed model can
      // never diverge from what's actually sent (the "picker says Minimax
      // but it used the default" bug after a restart / stale cache).
      if (modelName) {
        const sMeta = sessions.find(s => s.id === id);
        if (sMeta && sMeta.model !== modelName) {
          sMeta.model = modelName;
          updateModelPicker();
        }
      }
    }

    // Guard: if the fetched history is empty but the DOM already has message
    // bubbles for the same session (incognito doesn't persist, so /api/history
    // returns []), preserve the DOM instead of wiping it. This fixes the
    // "reply flashes for 0.1s then empty" bug when selectSession is called
    // after a streaming completion in an incognito chat.
    const isSameSession = (prevSessionId === id);
    const hasExistingBubbles = chatHistory && chatHistory.querySelectorAll('.msg').length > 0;
    const wouldWipe = !isOC && !msgHistory.length && isSameSession && hasExistingBubbles;
    if (wouldWipe) {
      // Skip the fade/reload; we're already showing the right content.
      if (chatHistory) chatHistory.classList.remove('no-animate');
      return;
    }

    if (paintedLoading && chatHistory) {
      _updateSessionLoading(chatHistory, msgHistory.length ? 'Rendering chat' : 'Opening chat');
      await _nextPaint();
      if (navToken !== _sessionNavToken || currentSessionId !== id) return;
      chatHistory.innerHTML = '';
    }

    // Fade out old content, swap, fade in. When we already painted a loading
    // state, keep it visible until render starts instead of fading to a blank
    // pane during slow history fetches.
    if (chatHistory && !paintedLoading) {
      chatHistory.style.transition = 'opacity 0.12s ease-out';
      chatHistory.style.opacity = '0';
      await new Promise(r => setTimeout(r, 120));
      if (navToken !== _sessionNavToken || currentSessionId !== id) return;
      chatHistory.innerHTML = '';
    }

    // Suppress per-message entrance animations during bulk history render
    if (chatHistory) chatHistory.classList.add('no-animate');

    // Populate new content while invisible
    if (isOC) {
      if (window.chatModule && window.chatModule.showWelcomeScreen) window.chatModule.showWelcomeScreen();
      window.chatModule.addMessage('assistant',
        `<p>\uD83E\uDD9E <strong>OpenClaw Agent Connected</strong></p>
         <p>Messages will be routed through your OpenClaw agent. The agent has access to tools, memory, and skills configured in your OpenClaw workspace.</p>`,
        'OpenClaw');
    } else if (msgHistory.length) {
      for (const msg of msgHistory) {
        try {
          _renderHistoryMessage(msg, modelName);
        } catch (e) {
          console.warn('Failed to render history message:', e, msg);
        }
      }
    } else {
      if (window.chatModule && window.chatModule.showWelcomeScreen) window.chatModule.showWelcomeScreen();
      // Don't highlight ordinary empty sessions — feels like nothing is
      // selected. Keep document/email-scoped sessions highlighted though: a
      // new email/reply chat starts empty but immediately owns an email doc.
      const isDocScopedEmptySession = !!(meta && (meta.has_documents || /^Email:|^New Email$/i.test(meta.name || '')));
      if (!isDocScopedEmptySession) {
        document.querySelectorAll('.list-item.active-session').forEach(el => el.classList.remove('active-session'));
      }
    }
    uiModule.scrollHistoryInstant();
    if (!isOC && msgHistory.length) {
      _installHistoryPager(id, pageInfo, modelName);
    }

    // Fade in and re-enable message animations
    if (chatHistory) {
      chatHistory.style.transition = 'opacity 0.15s ease-in';
      chatHistory.style.opacity = '1';
      chatHistory.classList.remove('no-animate');
    }
    if (window.hljs) {
      document.querySelectorAll('pre code:not(.hljs)').forEach(block => {
        window.hljs.highlightElement(block);
      });
    }
    // Hide research button on session switch — it's only for the session that started it
    var _rBtn = document.getElementById('research-toggle-btn');
    var _rChk = document.getElementById('research-toggle');
    if (_rBtn) _rBtn.style.display = 'none';
    if (_rChk) _rChk.checked = false;

    // Check for pending/completed research that survived a page refresh
    if (window.chatModule && window.chatModule.checkPendingResearch) {
      window.chatModule.checkPendingResearch(id);
    }
    // Restore group chat state if this is a group session
    if (window.groupModule && window.groupModule.restoreState && window.groupModule.restoreState(id)) {
      if (window._syncGroupIndicator) window._syncGroupIndicator(true);
      // Hide model picker for group sessions
      const _mpw = document.getElementById('model-picker-wrap');
      if (_mpw) _mpw.style.display = 'none';
    } else if (window.groupModule && window.groupModule.isActive()) {
      // Switching away from group session — deactivate
      window.groupModule.stopGroup();
      if (window._syncGroupIndicator) window._syncGroupIndicator(false);
    }

    // Stop pulsing notification — user is now viewing this session
    clearStreamComplete(id);

    // Re-attach any background stream
    try {
      if (window.chatModule && window.chatModule.checkBackgroundStream) {
        window.chatModule.checkBackgroundStream(id);
      }
    } catch (e) {
      console.warn('checkBackgroundStream error:', e);
    }
    // Check server for active stream (survives page refresh)
    _checkServerStream(id);
    // Document panel: keep open if next session also wants it, otherwise close
    if (window.documentModule) {
      const docBtn = document.getElementById('overflow-doc-btn');
      const meta = sessions.find(s => s.id === id);
      const shouldOpen = localStorage.getItem('odysseus-doc-open-' + id) === '1';
      const hasDocs = !!(meta && meta.has_documents);
      if (docBtn) {
        docBtn.classList.remove('active');
        docBtn.classList.toggle('has-docs', hasDocs);
      }
      const docInd = document.getElementById('doc-indicator-btn');
      if (docInd) docInd.classList.toggle('visible', hasDocs);
      if (hasDocs) {
        // Wait for session UI to settle, then slide in documents
        setTimeout(() => window.documentModule.loadSessionDocs(id, { restoreMode: true }), 300);
      } else if (!shouldOpen) {
        window.documentModule.closePanel();
      }
    }

  } catch (error) {
    console.error('Error in selectSession:', error);
    const chatHistory = uiModule.el('chat-history');
    if (chatHistory?.querySelector('.session-loading-state')) {
      chatHistory.innerHTML = '';
      chatHistory.style.opacity = '1';
      chatHistory.classList.remove('no-animate');
      const msg = document.createElement('div');
      msg.className = 'msg msg-ai';
      msg.innerHTML = `<div class="body">Failed to load this chat. ${uiModule.esc ? uiModule.esc(error.message || '') : ''}</div>`;
      chatHistory.appendChild(msg);
    }
    uiModule.showError('Failed to load session: ' + error.message);
  } finally {
    // Memory warmup must not block chat switching. The memories panel can load
    // on demand; this is only a delayed cache refresh when the foreground chat
    // is idle.
    if (window.memoryModule && window.memoryModule.loadMemories) {
      setTimeout(() => {
        const busy = !!window.__odysseusChatBusy
          || Date.now() < (window.__odysseusChatBusyUntil || 0)
          || !!document.querySelector('.send-btn[data-mode="streaming"], .send-btn.send-pending');
        if (!busy) window.memoryModule.loadMemories().catch(() => {});
      }, 2500);
    }
    // Auto-focus message input (unless session list has keyboard focus).
    // Skip on mobile — focusing the textarea pops up the on-screen keyboard,
    // which is intrusive when the user is just navigating between chats
    // (e.g. picking a chat from the Library). They can tap the input to
    // bring up the keyboard when they actually want to type.
    if (!_sessionListFocused && window.innerWidth > 768) {
      const msgInput = document.getElementById('message');
      if (msgInput) msgInput.focus();
    }
  }
}

// Pending session — stored locally until the first message is sent
let _pendingChat = null; // { url, modelId, endpointId }
let _pendingMaterializePromise = null;

async function _getPreferredDefaultChat() {
  let dc = null;
  try {
    dc = window.__odysseusDefaultChat || null;
  } catch (_) {}
  if (!dc || !dc.endpoint_url || !dc.model) {
    try {
      dc = JSON.parse(localStorage.getItem('odysseus-default-chat-cache') || 'null');
    } catch (_) {}
  }
  if (dc && dc.endpoint_url && dc.model) return dc;
  try {
    const dcRes = await fetch(`${API_BASE}/api/default-chat`);
    dc = await dcRes.json();
    if (dc && dc.endpoint_url && dc.model) {
      try {
        window.__odysseusDefaultChat = dc;
        localStorage.setItem('odysseus-default-chat-cache', JSON.stringify(dc));
      } catch (_) {}
      return dc;
    }
  } catch (_) {}
  return null;
}

export function createDirectChat(url, modelId, endpointId, opts = {}) {
  const incomingSource = opts.source || 'manual';
  if (
    _pendingChat &&
    _pendingChat.modelId &&
    _pendingChat.source === 'manual' &&
    incomingSource !== 'manual'
  ) {
    updateModelPicker();
    return;
  }
  _sessionNavToken++;
  // Detach any active stream so it doesn't interfere with the new chat
  if (window.chatModule && window.chatModule.detachCurrentStream) {
    window.chatModule.detachCurrentStream(currentSessionId);
  }
  // Stop an active GROUP chat too — otherwise its in-flight parallel/round-robin
  // streams keep rendering into the brand-new chat (abort the group's fetches).
  if (window.groupModule && window.groupModule.isActive && window.groupModule.isActive()) {
    try { window.groupModule.stopGroup(); } catch {}
    if (window._syncGroupIndicator) window._syncGroupIndicator(false);
  }

  // Don't hit the API — just store the model info and prepare the UI
  _pendingChat = { url, modelId, endpointId, source: incomingSource };
  _pendingMaterializePromise = null;
  _skipAutoSelect = true;
  _suppressNextSessionLoading = true;
  currentSessionId = null;
  try { window.__odysseusLastSelectedSessionId = ''; } catch (_) {}
  Storage.remove('lastSessionId');
  history.replaceState(null, '', window.location.pathname);
  document.querySelectorAll('.list-item.active-session, .session-item.active').forEach(el => {
    el.classList.remove('active-session', 'active');
  });

  // Close document panel — new chat has no docs
  if (window.documentModule && window.documentModule.isPanelOpen()) {
    window.documentModule.closePanel();
  }
  const docBtn = document.getElementById('overflow-doc-btn');
  if (docBtn) {
    docBtn.classList.remove('active', 'has-docs');
    docBtn.style.display = ''; // show in overflow menu again
  }
  const docInd = document.getElementById('doc-indicator-btn');
  if (docInd) docInd.classList.remove('visible', 'active');

  // Clear chat area and show welcome
  const box = document.getElementById('chat-history');
  if (box) box.innerHTML = '';
  if (window.chatModule && window.chatModule.showWelcomeScreen) {
    window.chatModule.showWelcomeScreen();
  }

  // Update model picker to show the pending model
  updateModelPicker();
  if (window.refreshChatContextHeader) window.refreshChatContextHeader('new-chat');

  // Update current-meta header
  const metaEl = document.getElementById('current-meta');
  if (metaEl) {
    metaEl.textContent = 'New Chat';
  }

  // Enable input
  const msgInput = document.getElementById('message');
  if (msgInput) {
    msgInput.disabled = false;
    _clearComposerUnlessStartupTyped(msgInput);
    msgInput.focus();
  }
}

/** Actually create the session in the DB. Called on first message send. */
export async function materializePendingSession() {
  if (_pendingMaterializePromise) return _pendingMaterializePromise;
  const pending = _pendingChat;
  if (!pending) return false;

  const materializePromise = (async () => {

    const incognitoChk = document.getElementById('incognito-toggle');
    const isIncognito = incognitoChk && incognitoChk.checked;
    const base = (pending.modelId || 'model').split('/').pop();
    const name = isIncognito ? 'Nobody' : `${base} ${new Date().toLocaleTimeString()}`;

    const fd = new FormData();
    fd.append('name', name);
    fd.append('endpoint_url', pending.url || '');
    fd.append('model', pending.modelId || '');
    if (pending.url && pending.modelId) {
      fd.append('skip_validation', 'true');
    }
    if (pending.endpointId) {
      fd.append('endpoint_id', pending.endpointId);
    }

    let res;
    try {
      res = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: fd });
    } catch (e) {
      uiModule.showError('Failed to reach backend: ' + e);
      return false;
    }

    let payload;
    try {
      payload = await res.json();
    } catch {
      payload = { detail: await res.text() };
    }

    if (!res.ok) {
      uiModule.showError(`Session create failed (${res.status}) ${payload.detail || JSON.stringify(payload)}`);
      return false;
    }

    // The user may have opened an existing chat while this deferred default
    // session was being created. Do not let a stale response steal
    // currentSessionId and make the next send land in a brand-new chat.
    if (_pendingChat !== pending) {
      if (payload.id) {
        fetch(`${API_BASE}/api/session/${encodeURIComponent(payload.id)}`, { method: 'DELETE' }).catch(() => {});
      }
      return false;
    }

    if (isIncognito && payload.id) {
      _markIncognito(payload.id);
    }

    // Clear any leftover document text selection from the previous session
    if (window.documentModule?.clearSelection) {
      try { window.documentModule.clearSelection(); } catch {}
    }
    _pendingChat = null;
    currentSessionId = payload.id;
    if (!isIncognito) {
      Storage.set('lastSessionId', payload.id);
    }

    // Reload the sidebar in the background. Awaiting this used to block the first
    // prompt in a new/pending chat behind startup fetches and slow /api/sessions
    // calls, so the user's message could sit for 20s+ before streaming began.
    _suppressNextSessionLoading = true;
    if (window.refreshChatContextHeader) window.refreshChatContextHeader('materialize-session');
    loadSessions().catch(() => {});
    return true;
  })();
  _pendingMaterializePromise = materializePromise;

  try {
    return await materializePromise;
  } finally {
    if (_pendingMaterializePromise === materializePromise) {
      _pendingMaterializePromise = null;
    }
  }
}

export function preMaterializePendingSession() {
  if (!_pendingChat || _pendingMaterializePromise) return;
  const incognitoChk = document.getElementById('incognito-toggle');
  if (incognitoChk && incognitoChk.checked) return;
  setTimeout(() => {
    const chk = document.getElementById('incognito-toggle');
    if (chk && chk.checked) return;
    if (_pendingChat && !_pendingMaterializePromise) {
      materializePendingSession().catch(() => {});
    }
  }, 250);
}

export function hasPendingChat() { return !!_pendingChat; }
export function getPendingChat() { return _pendingChat; }
// Getters for external access
export function getCurrentSessionId() {
  return currentSessionId;
}

export function isCurrentSessionIncognito() {
  return !!(currentSessionId && _isIncognitoSession(currentSessionId));
}

export function getSessions() {
  return sessions;
}

export function getCurrentModel() {
  const sess = sessions.find(x => x.id === currentSessionId);
  if (sess && sess.model) return sess.model;
  if (_pendingChat && _pendingChat.modelId) return _pendingChat.modelId;
  return null;
}

/** Endpoint URL serving the current (or pending) session's model. Used to
 *  decide whether a model is local (free) vs a billable cloud provider. */
export function getCurrentEndpointUrl() {
  const sess = sessions.find(x => x.id === currentSessionId);
  if (sess && sess.endpoint_url) return sess.endpoint_url;
  if (_pendingChat && _pendingChat.url) return _pendingChat.url;
  return null;
}

export function setCurrentSessionId(id) {
  _sessionNavToken++;
  currentSessionId = id;
  try { window.__odysseusLastSelectedSessionId = id || ''; } catch (_) {}
  if (!id) {
    _suppressNextSessionLoading = true;
    Storage.remove('lastSessionId');
    history.replaceState(null, '', window.location.pathname);
    document.querySelectorAll('.list-item.active-session, .session-item.active').forEach(el => {
      el.classList.remove('active-session', 'active');
    });
  }
}

export async function deleteCurrentSessionFromTopMenu() {
  const sid = currentSessionId;
  if (!sid) {
    uiModule.showToast('No chat to delete');
    return false;
  }
  const session = sessions.find(s => String(s.id) === String(sid));
  if (session?.is_important) {
    uiModule.showToast('Unfavorite before deleting');
    return false;
  }
  if (!await uiModule.styledConfirm('Delete this session?', { confirmText: 'Delete', danger: true })) {
    return false;
  }
  if (window.chatModule && window.chatModule.abortCurrentRequest) {
    window.chatModule.abortCurrentRequest();
  }
  _deselectCurrentSession(sid);
  _removeSessionFromLocalState(sid);
  _skipAutoSelect = true;
  try {
    const pm = await import('./presets.js');
    if (pm.removePersistentChat) pm.removePersistentChat(sid);
  } catch (e) {}
  try {
    const res = await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
    if (!res.ok) throw new Error('Failed');
    uiModule.showToast('Session deleted');
  } catch (e) {
    uiModule.showError('Failed to delete session');
  }
  await loadSessions();
  return true;
}

// Session list keyboard navigation: arrows to move, Delete to delete
async function _onSessionListKeydown(e) {
  const item = e.target.closest('.list-item[data-session-id]');
  if (!item) return;

  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
    e.preventDefault();
    // Get all visible session items across all containers
    const allItems = Array.from(document.querySelectorAll('#session-list .list-item[data-session-id]'));
    const idx = allItems.indexOf(item);
    if (idx < 0) return;
    const next = e.key === 'ArrowDown' ? allItems[idx + 1] : allItems[idx - 1];
    if (next) {
      next.focus();
      const sid = next.dataset.sessionId;
      if (sid) selectSession(sid);
    }
    return;
  }

  if (e.key === 'Delete' || e.key === 'Backspace') {
    if (item.querySelector('.session-rename-input')) return;
    e.preventDefault();
    const sid = item.dataset.sessionId;
    const s = sessions.find(x => x.id === sid);
    if (!s) return;
    if (s.is_important) {
      uiModule.showToast('Unfavorite before deleting');
      return;
    }
    const ok = await uiModule.styledConfirm('Delete this session?', { confirmText: 'Delete', danger: true });
    if (!ok) return;
    _sessionListFocused = true;
    (async () => {
      await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' });
      _deselectCurrentSession(s.id);
      await loadSessions();
    })();
    return;
  }

  if (e.key === 'Enter') {
    e.preventDefault();
    const sid = item.dataset.sessionId;
    if (sid) selectSession(sid);
    return;
  }
}

// Initialize drag sorting for sessions — uses the same dragSortModule as models
export function initDragSort() {
  if (!window.dragSortModule) return;
  const list = uiModule.el('session-list');
  if (!list) return;

  // Unfiled sessions (exclude items nested inside folders)
  window.dragSortModule.enable('session-list', '.list-item', {
    instanceKey: 'session-items',
    handleSelector: '.item-drag-handle',
    excludeSelector: '.session-folder-content .list-item',
    storageKey: 'session-order',
  });

  // Folder reordering
  window.dragSortModule.enable('session-list', '.session-folder', {
    instanceKey: 'session-folders',
    handleSelector: '.folder-drag-handle',
    onReorder: (items) => {
      const order = items.map(f => f.dataset.folderName).filter(Boolean);
      saveFolderOrder(order);
    },
  });

  // Sessions within each folder
  list.querySelectorAll('.session-folder-content').forEach((content, i) => {
    const id = 'session-folder-content-' + i;
    content.id = id;
    window.dragSortModule.enable(id, '.list-item', {
      handleSelector: '.item-drag-handle',
    });
  });
}

// Hash-based routing: navigate between sessions with browser back/forward.
// Skip entity-prefixed hashes (document-, note-, etc.) — those are handled
// by their own click handlers in chatRenderer.js and must not trigger
// session navigation (which would reset the active chat).
window.addEventListener('hashchange', () => {
  const hashId = window.location.hash.replace('#', '');
  if (/^(document|note|image|email|event|task|skill|research)-/.test(hashId) || /^open=notes&note=/.test(hashId)) return;
  if (hashId && hashId !== currentSessionId) {
    const target = sessions.find(s => s.id === hashId && !s.archived);
    if (target) selectSession(hashId);
  }
});

// ── Research indicator management ──
function _updateResearchDots() {
  document.querySelectorAll('.session-star[data-session-id]').forEach(function(star) {
    var sid = star.dataset.sessionId;
    var isRunning = _researchingSessions.has(sid) || _streamingSessions.has(sid);
    var isCompleted = _completedSessions.has(sid) && !isRunning;
    var listItem = star.closest('.list-item');
    star.classList.toggle('processing', isRunning);
    star.classList.toggle('notify', isCompleted);
    if (listItem) listItem.classList.toggle('stream-complete', isCompleted);

    if (isRunning || isCompleted) {
      star.style.opacity = '1';
    } else {
      star.style.opacity = '';
    }
  });
}

function _startResearchPolling() {
  if (_researchPollTimer) return;
  _researchPollTimer = setInterval(async function() {
    if (_researchingSessions.size === 0) {
      clearInterval(_researchPollTimer);
      _researchPollTimer = null;
      return;
    }
    for (var sid of _researchingSessions) {
      try {
        var res = await fetch(`${API_BASE}/api/research/status/${sid}`);
        if (!res.ok) { _researchingSessions.delete(sid); continue; }
        var data = await res.json();
        if (data.status !== 'running') {
          _researchingSessions.delete(sid);
        }
      } catch (e) {
        _researchingSessions.delete(sid);
      }
    }
    _updateResearchDots();
    if (_researchingSessions.size === 0 && _researchPollTimer) {
      clearInterval(_researchPollTimer);
      _researchPollTimer = null;
    }
  }, 5000);
}

export function markResearching(sessionId) {
  _researchingSessions.add(sessionId);
  _updateResearchDots();
  _updateRailNotifs();
  _startResearchPolling();
}

export function clearResearching(sessionId) {
  _researchingSessions.delete(sessionId);
  _updateResearchDots();
  _updateRailNotifs();
}

export function markStreaming(sessionId) {
  _streamingSessions.add(sessionId);
  _updateResearchDots();
  _updateRailNotifs();
}

export function clearStreaming(sessionId) {
  _streamingSessions.delete(sessionId);
  _updateResearchDots();
  _updateRailNotifs();
}

function _clearRunningState(sessionId) {
  if (!sessionId) return;
  var changed = false;
  if (_researchingSessions.delete(sessionId)) changed = true;
  if (_streamingSessions.delete(sessionId)) changed = true;
  if (changed) {
    _updateResearchDots();
    _updateRailNotifs();
  }
}

export function markStreamComplete(sessionId) {
  _researchingSessions.delete(sessionId);
  _streamingSessions.delete(sessionId);
  // Don't pulse if user is already viewing this session — they can see the response
  if (currentSessionId === sessionId) {
    _updateResearchDots();
    _updateRailNotifs();
    return;
  }
  _completedSessions.add(sessionId);
  _updateResearchDots();
  _updateRailNotifs();
  // Show notification dot on Chats section if collapsed
  const sessSection = document.getElementById('sessions-section');
  if (sessSection && sessSection.classList.contains('collapsed')) {
    const dot = document.getElementById('chats-notif-dot');
    if (dot) dot.style.display = 'inline-block';
  }
  // Safety net: re-apply after a tick in case a concurrent renderSessionList overwrites the DOM
  setTimeout(function() {
    if (_completedSessions.has(sessionId)) {
      _updateResearchDots();
    }
  }, 300);
}

// ── Rail notification dots ──
// Keep rail buttons lit when background work is happening / finished
function _updateRailNotifs() {
  // Research rail — pulsing while any session is researching
  const railResearch = document.getElementById('rail-research');
  if (railResearch) {
    // OR in the Deep Research panel's job state (set by panel.js)
    // so inline-research and panel-research both keep the rail lit.
    const researching = _researchingSessions.size > 0 || !!window._researchJobsActive;
    railResearch.classList.toggle('rail-notify', researching);
  }
  // Chats rail — show when a background stream completed
  const railChats = document.getElementById('rail-chats');
  if (railChats) {
    const sidebar = document.getElementById('sidebar');
    const sidebarHidden = sidebar && sidebar.classList.contains('hidden');
    const hasCompleted = _completedSessions.size > 0;
    railChats.classList.toggle('rail-notify', hasCompleted && sidebarHidden);
    railChats.classList.toggle('rail-notify-success', hasCompleted && sidebarHidden);
    // Store first completed session for click-to-open
    if (hasCompleted) {
      railChats.dataset.targetSession = [..._completedSessions][0];
    } else {
      delete railChats.dataset.targetSession;
    }
  }
  // Trigger rail sync so buttons become visible
  if (window._syncRailDynamic) window._syncRailDynamic();
}

/**
 * Check server for an active stream (survives page refresh).
 * If the server is still streaming for this session, show a spinner
 * and poll until done, then reload the session.
 */
async function _checkServerStream(sessionId) {
  try {
    // Skip if research is running — it has its own progress UI
    if (_researchingSessions.has(sessionId)) return;

    // Skip if the SSE reader is still actively connected — it handles rendering
    if (window.chatModule && window.chatModule.hasActiveStream && window.chatModule.hasActiveStream(sessionId)) return;

    const res = await fetch(`${API_BASE}/api/chat/stream_status/${sessionId}`);
    if (!res.ok) {
      _clearRunningState(sessionId);
      return; // 404 = no active stream
    }
    const info = await res.json();
    if (info.status !== 'streaming') {
      _clearRunningState(sessionId);
      return;
    }

    // Skip if this is a research stream — research has its own progress UI
    if (info.mode === 'research' || info.is_research) return;

    // Live-resume the detached run: replay its buffer then stream live tokens
    // (#2539). Falls back to the spinner+poll path below if unavailable.
    if (window.chatModule && window.chatModule.resumeStream) {
      const attached = await window.chatModule.resumeStream(sessionId);
      if (attached) return;
    }

    // Fallback: server is still streaming, show spinner and poll.
    const box = document.getElementById('chat-history');
    if (!box) return;

    const holder = document.createElement('div');
    holder.className = 'msg msg-ai';
    holder.innerHTML = '<div class="body"></div>';
    const bodyDiv = holder.querySelector('.body');

    const spinnerMod = await import('./spinner.js');
    const spinner = spinnerMod.default.create('Generating response...', 'right');
    bodyDiv.appendChild(spinner.createElement());
    spinner.start();
    box.appendChild(holder);
    uiModule.scrollHistory();

    // sessions.js executes before chat.js in module order, so window.chatModule
    // may not be set yet when _checkServerStream first runs. Retry resumeStream
    // on the first poll tick where it becomes available.
    let _resumeRetried = false;
    const pollId = setInterval(async () => {
      if (getCurrentSessionId() !== sessionId) {
        clearInterval(pollId);
        spinner.destroy();
        if (holder.parentNode) holder.remove();
        return;
      }
      if (!_resumeRetried && window.chatModule && window.chatModule.resumeStream) {
        _resumeRetried = true;
        const attached = await window.chatModule.resumeStream(sessionId);
        if (attached) {
          clearInterval(pollId);
          spinner.destroy();
          if (holder.parentNode) holder.remove();
          return;
        }
      }
      try {
        const r = await fetch(`${API_BASE}/api/chat/stream_status/${sessionId}`);
        if (!r.ok || (await r.json()).status !== 'streaming') {
          clearInterval(pollId);
          spinner.destroy();
          if (holder.parentNode) holder.remove();
          // Reload session to show the completed response + docs
          selectSession(sessionId);
        }
      } catch (_) {
        clearInterval(pollId);
        spinner.destroy();
        if (holder.parentNode) holder.remove();
        selectSession(sessionId);
      }
    }, 1500);
  } catch (_) {
    // No stream active — nothing to do
  }
}

export function clearStreamComplete(sessionId) {
  _completedSessions.delete(sessionId);
  // Direct DOM cleanup in case _updateResearchDots misses it
  var item = document.querySelector(`.list-item[data-session-id="${sessionId}"]`);
  if (item) item.classList.remove('stream-complete');
  var star = document.querySelector(`.session-star[data-session-id="${sessionId}"]`);
  if (star) { star.classList.remove('notify', 'processing'); star.style.opacity = ''; }
  _updateResearchDots();
  _updateRailNotifs();
}

// Initialize dropdowns once DOM is ready
function _initAllDropdowns() {
  initModelPicker({
    getCurrentSessionId: () => currentSessionId,
    getSessions: () => sessions,
    getPendingChat: () => _pendingChat,
    setPendingChat: (v) => { _pendingChat = v; },
    createDirectChat,
  });
  _initDropdownDismiss();
  _initBulkSelect();
}
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', _initAllDropdowns);
} else {
  _initAllDropdowns();
}

// Shared global listener to close all session dropdowns on click-away or Escape
function _initDropdownDismiss() {
  document.addEventListener('click', (e) => {
    if (e.target.closest('.session-dropdown-menu, .session-folder-submenu')) return;
    document.querySelectorAll('.session-dropdown-menu, .session-folder-submenu').forEach(d => d.style.display = 'none');
  });
  // Watch the sidebar — when it's hidden (any path: hamburger, swipe, mobile
  // collapse), close any open session dropdowns so they don't orphan over
  // the page.
  const _sb = document.getElementById('sidebar');
  if (_sb) {
    new MutationObserver(() => {
      if (_sb.classList.contains('hidden')) {
        document.querySelectorAll('.session-dropdown-menu, .session-folder-submenu').forEach(d => d.style.display = 'none');
      }
    }).observe(_sb, { attributes: true, attributeFilter: ['class'] });
  }
  document.addEventListener('keydown', (e) => {
    if (e.key !== 'Escape') return;
    // Esc must dismiss both the parent dropdown AND the Move-to-folder
    // submenu in one keypress — previously only the dropdown closed and
    // the submenu was left orphaned on screen.
    document.querySelectorAll('.session-dropdown-menu, .session-folder-submenu').forEach(d => d.style.display = 'none');
  });
}

// ──────────────────────────────────────────────
// Shared: positioned dropdown menu
// ──────────────────────────────────────────────

/**
 * Show a dropdown menu anchored to a button, using the existing
 * .dropdown / .dropdown-item-compact / .session-dropdown-menu CSS.
 * Items: [{ label, action, danger? }]
 * Returns a close() function.
 */
function _showDropdown(anchorEl, items) {
  // Close any open archive dropdown
  document.querySelectorAll('.session-dropdown-menu.archive-dd').forEach(d => d.remove());

  const dd = document.createElement('div');
  dd.className = 'dropdown session-dropdown-menu archive-dd';
  for (const item of items) {
    const row = document.createElement('div');
    row.className = 'dropdown-item-compact' + (item.danger ? ' dropdown-item-danger' : '');
    row.innerHTML = '<span>' + item.label + '</span>';
    row.addEventListener('click', (e) => {
      e.stopPropagation();
      close();
      item.action();
    });
    dd.appendChild(row);
  }
  document.body.appendChild(dd);

  // Position using viewport coords (same pattern as session menus)
  const rect = anchorEl.getBoundingClientRect();
  dd.style.right = (window.innerWidth - rect.right) + 'px';
  dd.style.top = '-9999px';
  dd.style.display = 'block';
  const ddRect = dd.getBoundingClientRect();
  if (rect.bottom + 2 + ddRect.height > window.innerHeight) {
    dd.style.top = Math.max(2, rect.top - ddRect.height - 2) + 'px';
  } else {
    dd.style.top = (rect.bottom + 2) + 'px';
  }

  function close() { dd.remove(); }
  // Existing _initDropdownDismiss handles click-away + Escape for .session-dropdown-menu
  return close;
}


// ──────────────────────────────────────────────
// Archive Browser
// ──────────────────────────────────────────────

// All mutable archive state lives here; reset on each openArchive().
const _arc = { data: [], total: 0, search: '', offset: 0, sort: 'recent', model: '', debounce: null, selectMode: false, selected: new Set(), allModelCounts: null };

function _arcRelativeTime(iso) {
  if (!iso) return '';
  const diff = Date.now() - new Date(iso).getTime();
  const mins = Math.floor(diff / 60000);
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  const days = Math.floor(hrs / 24);
  if (days < 30) return `${days}d ago`;
  return new Date(iso).toLocaleDateString();
}

// ── Actions (pure side-effects, no DOM creation) ──

// Peek at an archived session — load its history without unarchiving
let _peekingSessionId = null;

async function _arcPeekOpen(sid) {
  try {
    _peekingSessionId = sid;
    closeArchive();
    // Load history directly without unarchiving
    const res = await fetch(`${API_BASE}/api/history/${sid}`);
    const data = await res.json();
    const history = data.history || [];

    // Set as current session so chat renders
    currentSessionId = sid;

    // Find the archived session metadata
    const meta = _arc.data.find(s => s.id === sid);
    const metaEl = document.getElementById('current-meta');
    if (metaEl) metaEl.textContent = (meta?.name || 'Archived') + ' (archived)';

    // Render the chat history
    const chatBox = document.getElementById('chat-history');
    if (chatBox) chatBox.innerHTML = '';
    if (window.chatModule && window.chatModule.hideWelcomeScreen) window.chatModule.hideWelcomeScreen();

    const addMsg = window.chatModule && window.chatModule.addMessage;
    if (addMsg) {
      for (const msg of history) {
        if (msg.role === 'user' || msg.role === 'assistant') {
          const model = String((msg.metadata && msg.metadata.model) || '');
          const content = typeof msg.content === 'string' ? msg.content : (Array.isArray(msg.content) ? msg.content : String(msg.content || ''));
          try { addMsg(msg.role, content, model, msg.metadata || null); } catch (e) { console.warn('Failed to render message:', e); }
        }
      }
    }
    if (window.uiModule) window.uiModule.scrollHistory();
  } catch (e) {
    console.error('Peek open failed:', e);
    uiModule.showError('Failed to open archived session');
  }
}

// When navigating away from a peeked session, just clear the state
function _checkPeekCleanup(newSessionId) {
  if (_peekingSessionId && _peekingSessionId !== newSessionId) {
    _peekingSessionId = null;
  }
}

async function _arcRestore(sid) {
  try {
    const res = await fetch(`${API_BASE}/api/session/${sid}/unarchive`, { method: 'POST' });
    if (!res.ok) throw new Error('Failed');
    _arcRemove(sid);
    _arcRefreshUI();
    uiModule.showToast('Session restored');
    loadSessions();
  } catch { uiModule.showError('Failed to restore session'); }
}

async function _arcDelete(sid) {
  if (!await window.styledConfirm('Delete this session permanently?', { confirmText: 'Delete', danger: true })) return;
  try {
    const res = await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
    if (!res.ok) throw new Error('Failed');
    await _animateSessionRowsRemoving([sid], '#archive-grid .archive-row[data-session-id]');
    _arcRemove(sid);
    _arcRefreshUI();
    uiModule.showToast('Session deleted');
  } catch { uiModule.showError('Failed to delete session'); }
}

function _arcRemove(sid) {
  _arc.data = _arc.data.filter(x => x.id !== sid);
  _arc.total--;
  _arc.selected.delete(sid);
}

async function _arcBulkRestore() {
  const ids = [..._arc.selected];
  if (!ids.length) return;
  for (const sid of ids) {
    try {
      await fetch(`${API_BASE}/api/session/${sid}/unarchive`, { method: 'POST' });
      _arcRemove(sid);
    } catch {}
  }
  _arc.selected.clear();
  _arcRefreshUI();
  uiModule.showToast(`${ids.length} session${ids.length > 1 ? 's' : ''} restored`);
  loadSessions();
}

async function _arcBulkDelete() {
  const ids = [..._arc.selected];
  if (!ids.length) return;
  const ok = await uiModule.styledConfirm(`Delete ${ids.length} session${ids.length > 1 ? 's' : ''} permanently?`, { confirmText: 'Delete', danger: true });
  if (!ok) return;
  const deletedIds = [];
  for (const sid of ids) {
    try {
      const res = await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
      if (res.ok) {
        deletedIds.push(sid);
        _arcRemove(sid);
      }
    } catch {}
  }
  await _animateSessionRowsRemoving(deletedIds, '#archive-grid .archive-row[data-session-id]');
  _arc.selected.clear();
  _arcRefreshUI();
  uiModule.showToast(`${deletedIds.length} session${deletedIds.length > 1 ? 's' : ''} deleted`);
}

function _arcToggleSelectMode() {
  _arc.selectMode = !_arc.selectMode;
  _arc.selected.clear();
  _arcRefreshUI();
}

function _arcUpdateBulkBar() {
  const bar = document.getElementById('archive-bulk-bar');
  const count = document.getElementById('archive-selected-count');
  const selectBtn = document.getElementById('archive-select-btn');
  if (bar) bar.classList.toggle('hidden', !_arc.selectMode);
  if (count) count.textContent = `${_arc.selected.size} selected`;
  if (selectBtn) {
    selectBtn.textContent = _arc.selectMode ? 'Cancel' : 'Select';
    selectBtn.classList.toggle('active', _arc.selectMode);
  }
}

// ── Data fetching ──

async function _arcFetch(append) {
  if (!append) _arc.offset = 0;
  const params = new URLSearchParams({ offset: String(_arc.offset), limit: '20', sort: _arc.sort });
  if (_arc.search) params.set('search', _arc.search);
  if (_arc.model) params.set('model', _arc.model);
  try {
    const res = await fetch(`${API_BASE}/api/sessions/archived?${params}`);
    if (!res.ok) throw new Error(res.statusText);
    const data = await res.json();
    _arc.data = append ? _arc.data.concat(data.sessions) : data.sessions;
    _arc.total = data.total;
    // Cache model counts from unfiltered first fetch
    if (!_arc.allModelCounts && !_arc.model && !_arc.search) {
      const counts = {};
      _arc.data.forEach(s => {
        const m = (s.model || '').split('/').pop();
        if (m) counts[m] = (counts[m] || 0) + 1;
      });
      _arc.allModelCounts = { counts, total: _arc.total };
    }
    _arcRefreshUI();
  } catch (e) {
    console.error('Archive fetch failed:', e);
  }
}

// ── Rendering (dumb — reads _arc, writes DOM) ──

function _arcRefreshUI() {
  _arcRenderStats();
  _arcRenderChips();
  _arcRenderGrid();
  _arcRenderLoadMore();
  _arcUpdateBulkBar();
}

function _arcRenderStats() {
  const el = document.getElementById('archive-stats');
  if (el) el.textContent = _arc.total ? `${_arc.total}` : '';
}

function _arcRenderChips() {
  const el = document.getElementById('archive-chips');
  if (!el) return;
  // Use cached counts so chips don't disappear when filtering
  const cached = _arc.allModelCounts;
  if (!cached) return;
  const modelCounts = cached.counts;
  const models = Object.keys(modelCounts).sort();
  if (models.length < 2) { el.innerHTML = ''; return; }
  el.innerHTML = '';
  const mkChip = (label, value, count) => {
    const chip = document.createElement('button');
    chip.className = 'doclib-chip' + (_arc.model === value ? ' active' : '');
    chip.textContent = `${label} (${count})`;
    chip.addEventListener('click', () => { _arc.model = (_arc.model === value ? '' : value); _arcFetch(false); });
    el.appendChild(chip);
  };
  mkChip('All', '', cached.total);
  models.forEach(m => mkChip(m, m, modelCounts[m]));
}

function _arcRenderCard(s) {
  const card = document.createElement('div');
  card.className = 'memory-item archive-row' + (_arc.selected.has(s.id) ? ' selected' : '');
  card.dataset.sessionId = s.id;
  const modelShort = uiModule.esc((s.model || '').split('/').pop());
  const msgCount = s.message_count || 0;
  const checkboxHtml = _arc.selectMode
    ? `<input type="checkbox" class="memory-select-cb archive-checkbox" data-sid="${s.id}" ${_arc.selected.has(s.id) ? 'checked' : ''}>`
    : '';

  card.innerHTML = `
    ${checkboxHtml}
    <div style="flex:1;min-width:0;">
      <div class="memory-item-title">${uiModule.esc(s.name || 'Untitled')}</div>
      <div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">
        <span>${modelShort || 'no model'}</span>
        <span>\u00b7</span>
        <span>${msgCount} msg${msgCount !== 1 ? 's' : ''}</span>
        <span>\u00b7</span>
        <span>${_arcRelativeTime(s.updated_at)}</span>
      </div>
    </div>
    <div class="memory-item-actions">
      <button class="memory-item-btn archive-menu-btn" title="Actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button>
    </div>
  `;

  const checkbox = card.querySelector('.archive-checkbox');
  if (checkbox) {
    checkbox.addEventListener('change', (e) => {
      e.stopPropagation();
      if (e.target.checked) _arc.selected.add(s.id);
      else _arc.selected.delete(s.id);
      card.classList.toggle('selected', e.target.checked);
      _arcUpdateBulkBar();
    });
  }
  card.querySelector('.archive-menu-btn').addEventListener('click', (e) => {
    e.stopPropagation();
    _showDropdown(e.currentTarget, [
      { label: 'Open', action: () => _arcPeekOpen(s.id) },
      { label: 'Restore', action: () => _arcRestore(s.id) },
      { label: 'Delete', action: () => _arcDelete(s.id), danger: true },
    ]);
  });
  card.addEventListener('click', () => {
    if (_arc.selectMode) {
      if (_arc.selected.has(s.id)) _arc.selected.delete(s.id);
      else _arc.selected.add(s.id);
      const cb = card.querySelector('.archive-checkbox');
      if (cb) cb.checked = _arc.selected.has(s.id);
      card.classList.toggle('selected', _arc.selected.has(s.id));
      _arcUpdateBulkBar();
    } else {
      _arcPeekOpen(s.id);
    }
  });
  return card;
}

function _arcRenderGrid() {
  const grid = document.getElementById('archive-grid');
  if (!grid) return;
  if (_arc.data.length === 0) {
    grid.innerHTML = '<div class="doclib-empty">No archived sessions</div>';
    return;
  }
  grid.innerHTML = '';
  for (const s of _arc.data) grid.appendChild(_arcRenderCard(s));
}

function _arcRenderLoadMore() {
  const btn = document.getElementById('archive-load-more');
  if (!btn) return;
  btn.style.display = _arc.data.length < _arc.total ? '' : 'none';
}


// ── Unified Library Modal (Chats / Documents / Archive) ──

const _lib = { tab: 'chats', search: '', sort: 'recent', debounce: null, selectMode: false, selected: new Set() };

export function openLibrary(defaultTab) {
  // Delegate everything to the document module's library (has tabs for Chats/Documents/Archive)
  if (window.documentModule && window.documentModule.openLibrary) {
    window.documentModule.openLibrary({ tab: defaultTab || 'documents' });
    return;
  }
  if (document.getElementById('library-modal')) return;
  Object.assign(_lib, { tab: defaultTab || 'chats', search: '', sort: 'recent', debounce: null, selectMode: false, selected: new Set() });

  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.id = 'library-modal';
  modal.innerHTML = `
    <div class="modal-content doclib-modal-content">
      <div class="modal-header">
        <h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>Library <span id="lib-stats" style="font-size:0.8em;opacity:0.5;font-weight:normal;margin-left:4px"></span></h4>
        <button class="close-btn" id="lib-close">✖</button>
      </div>
      <div class="modal-body">
        <div class="lib-tabs" id="lib-tabs">
          <button class="lib-tab${_lib.tab === 'chats' ? ' active' : ''}" data-lib-tab="chats">Chats</button>
          <button class="lib-tab${_lib.tab === 'documents' ? ' active' : ''}" data-lib-tab="documents">Documents</button>
          <button class="lib-tab${_lib.tab === 'archive' ? ' active' : ''}" data-lib-tab="archive">Archive</button>
          <button class="lib-tab${_lib.tab === 'research' ? ' active' : ''}" data-lib-tab="research">Research</button>
        </div>
        <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
          <select class="memory-sort-select" id="lib-sort">
            <option value="recent">Recent</option>
            <option value="oldest">Oldest</option>
            <option value="most-messages">Most messages</option>
            <option value="alpha">A\u2013Z</option>
          </select>
          <input type="text" class="memory-search-input" id="lib-search" placeholder="Filter\u2026" style="flex:1;" />
          <button class="memory-toolbar-btn" id="lib-select-btn" title="Select">Select</button>
        </div>
        <div class="memory-bulk-bar hidden" id="lib-bulk-bar">
          <label class="memory-bulk-check-all"><input type="checkbox" id="lib-select-all"> All</label>
          <span id="lib-selected-count" style="color:color-mix(in srgb, var(--fg) 50%, transparent);font-size:10px;flex:1;">0 selected</span>
          <button class="memory-toolbar-btn" id="lib-bulk-action1"></button>
          <button class="memory-toolbar-btn danger" id="lib-bulk-delete">Delete</button>
        </div>
        <div class="doclib-grid archive-list" id="lib-grid"></div>
      </div>
    </div>
  `;
  document.body.appendChild(modal);

  // Draggable
  const _clContent = modal.querySelector('.modal-content');
  const _clHeader = modal.querySelector('.modal-header');
  if (themeModule && themeModule.makeDraggable && _clContent && _clHeader) {
    themeModule.makeDraggable(_clContent, _clHeader);
  }

  document.getElementById('lib-close').addEventListener('click', closeLibrary);

  // Tab switching
  modal.querySelectorAll('.lib-tab').forEach(tab => {
    tab.addEventListener('click', () => {
      // Documents tab — open the document module's library (has expand/preview)
      if (tab.dataset.libTab === 'documents' && window.documentModule && window.documentModule.openLibrary) {
        closeLibrary();
        window.documentModule.openLibrary();
        return;
      }
      _lib.tab = tab.dataset.libTab;
      _lib.search = '';
      _lib.selectMode = false;
      _lib.selected.clear();
      modal.querySelectorAll('.lib-tab').forEach(t => t.classList.remove('active'));
      tab.classList.add('active');
      document.getElementById('lib-search').value = '';
      document.getElementById('lib-bulk-bar').classList.add('hidden');
      // Update bulk action button label based on tab
      const action1 = document.getElementById('lib-bulk-action1');
      if (_lib.tab === 'archive') { action1.textContent = 'Restore'; }
      else if (_lib.tab === 'chats') { action1.textContent = 'Archive'; }
      else if (_lib.tab === 'research') { action1.textContent = 'Open Report'; }
      else { action1.textContent = 'Export'; }
      _renderLibGrid();
    });
  });

  // Set initial bulk action label
  const _initAction = document.getElementById('lib-bulk-action1');
  if (_initAction) _initAction.textContent = _lib.tab === 'archive' ? 'Restore' : _lib.tab === 'documents' ? 'Export' : 'Archive';

  document.getElementById('lib-sort').addEventListener('change', () => { _lib.sort = document.getElementById('lib-sort').value; _renderLibGrid(); });
  document.getElementById('lib-search').addEventListener('input', (e) => {
    clearTimeout(_lib.debounce);
    _lib.debounce = setTimeout(() => { _lib.search = e.target.value.trim().toLowerCase(); _renderLibGrid(); }, 200);
  });

  // Select mode
  document.getElementById('lib-select-btn').addEventListener('click', () => {
    _lib.selectMode = !_lib.selectMode;
    _lib.selected.clear();
    document.getElementById('lib-bulk-bar').classList.toggle('hidden', !_lib.selectMode);
    _renderLibGrid();
  });
  document.getElementById('lib-select-all').addEventListener('change', (e) => {
    const checked = e.target.checked;
    document.querySelectorAll('#lib-grid .memory-select-cb').forEach(cb => { cb.checked = checked; });
    document.querySelectorAll('#lib-grid .doclib-card').forEach(card => {
      const id = card.dataset.sessionId || card.dataset.docId;
      if (id) { if (checked) _lib.selected.add(id); else _lib.selected.delete(id); }
    });
    _updateLibCount();
  });

  // Bulk action 1 (Archive/Restore/Export)
  document.getElementById('lib-bulk-action1').addEventListener('click', async () => {
    if (_lib.tab === 'chats') {
      for (const sid of _lib.selected) await fetch(`${API_BASE}/api/session/${sid}/archive`, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
      uiModule.showToast(`Archived ${_lib.selected.size} sessions`);
    } else if (_lib.tab === 'archive') {
      for (const sid of _lib.selected) await fetch(`${API_BASE}/api/session/${sid}/restore`, { method: 'POST' });
      uiModule.showToast(`Restored ${_lib.selected.size} sessions`);
    }
    _lib.selected.clear();
    _lib.selectMode = false;
    document.getElementById('lib-bulk-bar').classList.add('hidden');
    await loadSessions();
    _renderLibGrid();
  });

  // Bulk delete
  document.getElementById('lib-bulk-delete').addEventListener('click', async () => {
    if (!await uiModule.styledConfirm(`Delete ${_lib.selected.size} items?`, { confirmText: 'Delete', danger: true })) return;
    if (_lib.tab === 'chats' || _lib.tab === 'archive') {
      for (const sid of _lib.selected) await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
    } else if (_lib.tab === 'documents') {
      for (const did of _lib.selected) await fetch(`${API_BASE}/api/document/${did}`, { method: 'DELETE' });
    } else if (_lib.tab === 'research') {
      for (const rid of _lib.selected) await fetch(`${API_BASE}/api/research/${rid}`, { method: 'DELETE' });
    }
    _lib.selected.clear();
    _lib.selectMode = false;
    document.getElementById('lib-bulk-bar').classList.add('hidden');
    await loadSessions();
    _renderLibGrid();
  });

  _renderLibGrid();
}

function _updateLibCount() {
  const el = document.getElementById('lib-selected-count');
  if (el) el.textContent = `${_lib.selected.size} selected`;
}

function _renderLibGrid() {
  const grid = document.getElementById('lib-grid');
  if (!grid) return;

  if (_lib.tab === 'chats') _renderLibChats(grid);
  else if (_lib.tab === 'archive') _renderLibArchive(grid);
  else if (_lib.tab === 'documents') _renderLibDocuments(grid);
  else if (_lib.tab === 'research') _renderLibResearch(grid);
}

function _renderLibChats(grid) {
  if (!sessions || !sessions.length) {
    grid.innerHTML = '<div class="doclib-empty">No sessions loaded</div>';
    return;
  }
  let filtered = sessions.filter(s => !s.archived);
  if (_lib.search) {
    const q = _lib.search;
    filtered = filtered.filter(s => (s.name || '').toLowerCase().includes(q) || (s.model || '').toLowerCase().includes(q));
  }
  if (_lib.sort === 'oldest') filtered.sort((a, b) => (a.created_at || '') > (b.created_at || '') ? 1 : -1);
  else if (_lib.sort === 'most-messages') filtered.sort((a, b) => (b.message_count || 0) - (a.message_count || 0));
  else if (_lib.sort === 'alpha') filtered.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
  else filtered.sort((a, b) => (b.updated_at || '') > (a.updated_at || '') ? 1 : -1);

  const stats = document.getElementById('lib-stats');
  if (stats) stats.textContent = `(${filtered.length})`;

  if (!filtered.length) { grid.innerHTML = '<div class="doclib-empty">No chats found</div>'; return; }
  grid.innerHTML = '';
  for (const s of filtered) {
    const card = _buildLibCard(s.id, s.name || 'Untitled', s.message_count || 0, (s.model || '').split('/').pop(), s.updated_at, s.id === currentSessionId);
    card.addEventListener('click', (e) => {
      if (e.target.closest('.archive-menu-btn,.memory-select-cb')) return;
      if (_lib.selectMode) { _toggleLibSelect(card, s.id); return; }
      closeLibrary(); selectSession(s.id);
    });
    card.querySelector('.archive-menu-btn').addEventListener('click', (e) => {
      e.stopPropagation();
      _showDropdown(e.currentTarget, [
        { label: 'Open', action: () => { closeLibrary(); selectSession(s.id); } },
        { label: 'Archive', action: async () => { await fetch(`${API_BASE}/api/session/${s.id}/archive`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); await loadSessions(); _renderLibGrid(); } },
        { label: 'Delete', action: async () => { if (!await uiModule.styledConfirm('Delete?', { confirmText: 'Delete', danger: true })) return; await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' }); await loadSessions(); _renderLibGrid(); }, danger: true },
      ]);
    });
    grid.appendChild(card);
  }
}

async function _renderLibArchive(grid) {
  grid.innerHTML = '';
  grid.appendChild(spinnerModule.createLoadingRow('Loading…'));
  try {
    const params = new URLSearchParams({ limit: '50', sort: _lib.sort === 'most-messages' ? 'messages' : _lib.sort });
    if (_lib.search) params.set('search', _lib.search);
    const res = await fetch(`${API_BASE}/api/sessions/archived?${params}`);
    const data = await res.json();
    const items = data.sessions || [];
    const stats = document.getElementById('lib-stats');
    if (stats) stats.textContent = `(${data.total || items.length})`;
    if (!items.length) { grid.innerHTML = '<div class="doclib-empty">No archived sessions</div>'; return; }
    grid.innerHTML = '';
    for (const s of items) {
      const card = _buildLibCard(s.id, s.name || 'Untitled', s.message_count || 0, (s.model || '').split('/').pop(), s.updated_at);
      card.addEventListener('click', (e) => {
        if (e.target.closest('.archive-menu-btn,.memory-select-cb')) return;
        if (_lib.selectMode) { _toggleLibSelect(card, s.id); return; }
      });
      card.querySelector('.archive-menu-btn').addEventListener('click', (e) => {
        e.stopPropagation();
        _showDropdown(e.currentTarget, [
          { label: 'Restore', action: async () => { await fetch(`${API_BASE}/api/session/${s.id}/restore`, { method: 'POST' }); await loadSessions(); _renderLibGrid(); } },
          { label: 'Delete', action: async () => { if (!await uiModule.styledConfirm('Delete?', { confirmText: 'Delete', danger: true })) return; await fetch(`${API_BASE}/api/session/${s.id}`, { method: 'DELETE' }); _renderLibGrid(); }, danger: true },
        ]);
      });
      grid.appendChild(card);
    }
  } catch (e) { console.error('Library archive error:', e); grid.innerHTML = '<div class="doclib-empty">Failed to load archive</div>'; }
}

async function _renderLibDocuments(grid) {
  grid.innerHTML = '';
  grid.appendChild(spinnerModule.createLoadingRow('Loading…'));
  try {
    const params = new URLSearchParams({ limit: '50', sort: _lib.sort });
    if (_lib.search) params.set('search', _lib.search);
    const res = await fetch(`${API_BASE}/api/documents/library?${params}`);
    const data = await res.json();
    const docs = data.documents || [];
    const stats = document.getElementById('lib-stats');
    if (stats) stats.textContent = `(${data.total || docs.length})`;
    if (!docs.length) { grid.innerHTML = '<div class="doclib-empty">No documents found</div>'; return; }
    grid.innerHTML = '';
    for (const d of docs) {
      const card = _buildLibCard(d.id, d.title || 'Untitled', d.version_count || 1, d.language || 'text', d.updated_at, false, true);
      card.dataset.docId = d.id;
      card.addEventListener('click', (e) => {
        if (e.target.closest('.archive-menu-btn,.memory-select-cb')) return;
        if (_lib.selectMode) { _toggleLibSelect(card, d.id); return; }
        // Open document in its session
        if (d.session_id && window.documentModule) {
          closeLibrary();
          selectSession(d.session_id);
          setTimeout(() => { if (window.documentModule.loadSessionDocs) window.documentModule.loadSessionDocs(d.session_id); }, 300);
        }
      });
      card.querySelector('.archive-menu-btn').addEventListener('click', (e) => {
        e.stopPropagation();
        _showDropdown(e.currentTarget, [
          { label: 'Open', action: () => { if (d.session_id) { closeLibrary(); selectSession(d.session_id); } } },
          { label: 'Delete', action: async () => { if (!await uiModule.styledConfirm('Delete?', { confirmText: 'Delete', danger: true })) return; await fetch(`${API_BASE}/api/document/${d.id}`, { method: 'DELETE' }); _renderLibGrid(); }, danger: true },
        ]);
      });
      grid.appendChild(card);
    }
  } catch (e) { console.error('Library documents error:', e); grid.innerHTML = '<div class="doclib-empty">Failed to load documents</div>'; }
}

async function _renderLibResearch(grid) {
  grid.innerHTML = '';
  grid.appendChild(spinnerModule.createLoadingRow('Loading research…'));
  try {
    const params = new URLSearchParams({ limit: '50', sort: _lib.sort });
    if (_lib.search) params.set('search', _lib.search);
    const res = await fetch(`${API_BASE}/api/research/library?${params}`);
    if (!res.ok) throw new Error(res.status);
    const data = await res.json();
    const items = data.research || [];
    const statsEl = document.getElementById('lib-stats');
    if (statsEl) statsEl.textContent = `${data.total || 0} research`;
    grid.innerHTML = '';
    if (!items.length) {
      grid.innerHTML = '<div class="doclib-empty">No research found</div>';
      return;
    }
    for (const item of items) {
      const meta = [
        item.duration || '',
        item.rounds ? item.rounds + ' rounds' : '',
      ].filter(Boolean).join(' \u00b7 ');
      const card = _buildLibCard(
        item.id, item.query || '(untitled)', item.source_count || 0,
        meta, item.completed_at ? new Date(item.completed_at * 1000).toISOString() : '',
        false, false,
      );
      const metaEl = card.querySelector('.memory-item-meta');
      if (metaEl) metaEl.textContent = metaEl.textContent.replace(/\d+ msgs?/, (item.source_count || 0) + ' sources');
      card.addEventListener('click', (e) => {
        if (e.target.closest('.archive-menu-btn') || e.target.closest('.memory-select-cb')) return;
        window.open(`${API_BASE}/api/research/report/${item.id}`, '_blank');
      });
      const menuBtn = card.querySelector('.archive-menu-btn');
      if (menuBtn) {
        menuBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          _showDropdown(e.currentTarget, [
            { label: 'Open Report', action: () => window.open(`${API_BASE}/api/research/report/${item.id}`, '_blank') },
            { label: 'Re-run', action: () => {
              const modal = document.getElementById('library-modal');
              if (modal) modal.style.display = 'none';
              const msgInput = document.getElementById('message');
              if (msgInput) { msgInput.value = item.query; msgInput.focus(); }
              uiModule.showToast('Toggle Research and send to re-run');
            }},
            { label: 'Delete', danger: true, action: async () => {
              if (!await window.styledConfirm('Delete this research?', { confirmText: 'Delete', danger: true })) return;
              await fetch(`${API_BASE}/api/research/${item.id}`, { method: 'DELETE' });
              _renderLibGrid();
            }},
          ]);
        });
      }
      grid.appendChild(card);
    }
  } catch (e) { console.error('Library research error:', e); grid.innerHTML = '<div class="doclib-empty">Failed to load research</div>'; }
}

function _buildLibCard(id, title, count, meta, time, isActive, isDoc) {
  const card = document.createElement('div');
  card.className = 'memory-item';
  card.dataset.sessionId = id;
  if (isDoc) card.dataset.docId = id;
  const cbHtml = _lib.selectMode ? `<input type="checkbox" class="memory-select-cb"${_lib.selected.has(id) ? ' checked' : ''}>` : '';
  const metaParts = [];
  if (meta) metaParts.push(uiModule.esc(meta));
  metaParts.push(isDoc ? 'v' + count : count + ' msg' + (count !== 1 ? 's' : ''));
  if (time) metaParts.push(_arcRelativeTime(time));
  card.innerHTML = `
    ${cbHtml}
    <div style="flex:1;min-width:0;">
      <div class="memory-item-title"${isActive ? ' style="color:var(--accent);"' : ''}>${uiModule.esc(title)}</div>
      <div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">${metaParts.join(' \u00b7 ')}</div>
    </div>
    <div class="memory-item-actions">
      <button class="memory-item-btn archive-menu-btn" title="Actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button>
    </div>
  `;
  const cb = card.querySelector('.memory-select-cb');
  if (cb) {
    cb.addEventListener('click', (e) => e.stopPropagation());
    cb.addEventListener('change', () => { if (cb.checked) _lib.selected.add(id); else _lib.selected.delete(id); _updateLibCount(); });
  }
  return card;
}

function _toggleLibSelect(card, id) {
  const cb = card.querySelector('.memory-select-cb');
  if (cb) { cb.checked = !cb.checked; if (cb.checked) _lib.selected.add(id); else _lib.selected.delete(id); _updateLibCount(); }
}

export function closeLibrary() {
  const modal = document.getElementById('library-modal');
  if (modal) {
    const content = modal.querySelector('.modal-content');
    if (content) {
      content.classList.add('modal-closing');
      content.addEventListener('animationend', () => modal.remove(), { once: true });
      setTimeout(() => { if (modal.parentElement) modal.remove(); }, 250);
    } else {
      modal.remove();
    }
  }
}

export function openArchive() {
  if (document.getElementById('archive-modal')) return;
  Object.assign(_arc, { data: [], total: 0, search: '', offset: 0, sort: 'recent', model: '', debounce: null, selectMode: false, selected: new Set(), allModelCounts: null });

  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.id = 'archive-modal';
  modal.innerHTML = `
    <div class="modal-content doclib-modal-content">
      <div class="modal-header">
        <h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>Archive <span id="archive-stats" style="font-size:0.8em;opacity:0.5;font-weight:normal;margin-left:4px"></span></h4>
        <button class="close-btn" id="archive-close">✖</button>
      </div>
      <div class="modal-body">
        <div class="doclib-chips" id="archive-chips"></div>
        <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
          <select class="memory-sort-select" id="archive-sort">
            <option value="recent">Recent</option>
            <option value="oldest">Oldest</option>
            <option value="most-messages">Most messages</option>
            <option value="alpha">A\u2013Z</option>
          </select>
          <input type="text" class="memory-search-input" id="archive-search" placeholder="Filter\u2026" style="flex:1;" />
          <button class="memory-toolbar-btn" id="archive-select-btn" title="Select sessions">Select</button>
        </div>
        <div class="memory-bulk-bar hidden" id="archive-bulk-bar">
          <label class="memory-bulk-check-all"><input type="checkbox" id="archive-select-all"> All</label>
          <span id="archive-selected-count" style="color:color-mix(in srgb, var(--fg) 50%, transparent);font-size:10px;flex:1;">0 selected</span>
          <button class="memory-toolbar-btn" id="archive-bulk-restore">Restore</button>
          <button class="memory-toolbar-btn danger" id="archive-bulk-delete">Delete</button>
        </div>
        <div class="doclib-grid archive-list" id="archive-grid"></div>
        <button class="doclib-load-more" id="archive-load-more" style="display:none">Load more</button>
      </div>
    </div>
  `;
  document.body.appendChild(modal);

  // Make draggable via header
  const _arcContent = modal.querySelector('.modal-content');
  const _arcHeader = modal.querySelector('.modal-header');
  if (themeModule && themeModule.makeDraggable && _arcContent && _arcHeader) {
    themeModule.makeDraggable(_arcContent, _arcHeader);
  }

  document.getElementById('archive-close').addEventListener('click', closeArchive);
  document.getElementById('archive-sort').addEventListener('change', (e) => { _arc.sort = e.target.value; _arcFetch(false); });
  document.getElementById('archive-search').addEventListener('input', (e) => {
    clearTimeout(_arc.debounce);
    _arc.debounce = setTimeout(() => { _arc.search = e.target.value.trim(); _arcFetch(false); }, 300);
  });
  document.getElementById('archive-load-more').addEventListener('click', () => { _arc.offset = _arc.data.length; _arcFetch(true); });
  document.getElementById('archive-select-btn').addEventListener('click', _arcToggleSelectMode);
  document.getElementById('archive-bulk-restore').addEventListener('click', _arcBulkRestore);
  document.getElementById('archive-bulk-delete').addEventListener('click', _arcBulkDelete);
  document.getElementById('archive-select-all').addEventListener('change', (e) => {
    if (e.target.checked) _arc.data.forEach(s => _arc.selected.add(s.id));
    else _arc.selected.clear();
    _arcRefreshUI();
  });
  modal.addEventListener('click', (e) => { if (uiModule.isTouchInsideModal()) return; if (e.target === modal) closeArchive(); });

  _arcFetch(false);
}

export function closeArchive() {
  const modal = document.getElementById('archive-modal');
  if (modal) {
    const content = modal.querySelector('.modal-content');
    if (content) {
      content.classList.add('modal-closing');
      content.addEventListener('animationend', () => modal.remove(), { once: true });
      setTimeout(() => { if (modal.parentElement) modal.remove(); }, 250);
    } else {
      modal.remove();
    }
  }
}

/** Update has_documents flag for a session and re-render the sidebar icon */
export function getSortMode() { return _sortMode; }
export function setSortMode(mode) {
  _sortMode = mode || null;
  if (mode) Storage.set('odysseus-session-sort', mode);
  else Storage.remove('odysseus-session-sort');
  renderSessionList();
}

export function setSessionHasDocs(sessionId, hasDocs) {
  const s = sessions.find(s => s.id === sessionId);
  if (s && s.has_documents !== hasDocs) {
    s.has_documents = hasDocs;
    renderSessionList();
  }
}

// Export all functions to window for use in main app
const sessionModule = {
  initDependencies,
  renderSessionList,
  loadSessions,
  selectSession,
  createDirectChat,
  materializePendingSession,
  preMaterializePendingSession,
  hasPendingChat,
  getPendingChat,
  getCurrentSessionId,
  getSessions,
  getCurrentModel,
  getCurrentEndpointUrl,
  setCurrentSessionId,
  initDragSort,
  updateModelPicker,
  markResearching,
  clearResearching,
  markStreaming,
  clearStreaming,
  markStreamComplete,
  clearStreamComplete,
  openLibrary,
  closeLibrary,
  openArchive,
  closeArchive,
  setSessionHasDocs,
  getSortMode,
  setSortMode,
  deleteCurrentSessionFromTopMenu
};

export { updateModelPicker };

export default sessionModule;
