/* sessions.ts — Session management, list rendering, and project helpers */
/// <reference path="./global.d.ts" />

// ── Session action icons (SVG, monochrome, inherit currentColor) ──
const ICONS = {
  pin:      '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" stroke="none"><polygon points="8,1.5 9.8,5.8 14.5,6.2 11,9.4 12,14 8,11.5 4,14 5,9.4 1.5,6.2 6.2,5.8"/></svg>',
  unpin:    '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><polygon points="8,2 9.8,6.2 14.2,6.2 10.7,9.2 12,13.8 8,11 4,13.8 5.3,9.2 1.8,6.2 6.2,6.2"/></svg>',
  folder:   '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M2 4.5h4l1.5 1.5H14v7H2z"/></svg>',
  archive:  '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="1.5" y="2" width="13" height="3" rx="1"/><path d="M2.5 5v8h11V5"/><line x1="6" y1="8.5" x2="10" y2="8.5"/></svg>',
  unarchive:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="1.5" y="2" width="13" height="3" rx="1"/><path d="M2.5 5v8h11V5"/><polyline points="6.5,7 8,5.5 9.5,7"/></svg>',
  dup:      '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="4.5" y="4.5" width="8.5" height="8.5" rx="1.5"/><path d="M3 11.5V3h8.5"/></svg>',
  trash:    '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M3.5 4.5h9M6.5 4.5V3h3v1.5M4.5 4.5v8.5h7v-8.5"/><line x1="7" y1="7" x2="7" y2="11"/><line x1="9" y1="7" x2="9" y2="11"/></svg>',
  more:     '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" stroke="none"><circle cx="8" cy="3" r="1.25"/><circle cx="8" cy="8" r="1.25"/><circle cx="8" cy="13" r="1.25"/></svg>',
};

async function newSession(flash?: boolean, agentOverride?: string): Promise<void> {
  updateQueueBadge();
  S.toolCalls = [];
  clearLiveToolCards();
  const inheritWs = S._profileDefaultWorkspace || (S.session ? S.session.workspace : null);
  S._profileDefaultWorkspace = null;
  const body: Record<string, unknown> = { model: ($('modelSelect') as unknown as HTMLSelectElement | null)?.value || '', workspace: inheritWs };
  const agentFromStorage = localStorage.getItem('hermes-webui-agent');
  const agentToSend = agentOverride || agentFromStorage || undefined;
  if (agentToSend) body.agent = agentToSend;
  const data = await api('/api/session/new', { method: 'POST', body: JSON.stringify(body) }) as { session: Session };
  S.session = data.session;
  S.messages = (data.session.messages || []) as Message[];
  S.lastUsage = { ...(data.session.last_usage || {}) } as Record<string, unknown>;
  if (flash) S.session!._flash = true;
  localStorage.setItem('hermes-webui-session', S.session.session_id);
  S.busy = false;
  S.activeStreamId = null;
  updateSendBtn();
  const _cb = $('btnCancel'); if (_cb) _cb.style.display = 'none';
  setStatus('');
  setComposerStatus('');
  updateQueueBadge(S.session.session_id);
  syncTopbar(); renderMessages(); loadDir('.');
}

async function loadSession(sid: string): Promise<void> {
  if (S.session && S.busy) { stopApprovalPolling(); stopClarifyPolling(); cancelStream(); }
  stopApprovalPolling(); hideApprovalCard();
  if (typeof stopClarifyPolling === 'function') stopClarifyPolling();
  if (typeof hideClarifyCard === 'function') hideClarifyCard();
  const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}`) as { session: Session };
  S.session = data.session;
  S.lastUsage = { ...(data.session.last_usage || {}) } as Record<string, unknown>;
  localStorage.setItem('hermes-webui-session', S.session.session_id);
  data.session.messages = ((data.session.messages || []) as Message[]).filter((m: Message) => m && m.role);
  const hasMessageToolMetadata = ((data.session.messages || []) as Message[]).some((m: Message) => {
    if (!m || m.role !== 'assistant') return false;
    const hasTc = Array.isArray(m.tool_calls) && m.tool_calls.length > 0;
    const hasTu = Array.isArray(m.content) && (m.content as unknown[]).some((p: unknown) => p && (p as { type?: string }).type === 'tool_use');
    return hasTc || hasTu;
  });
  const activeStreamId = data.session.active_stream_id || null;
  if (!INFLIGHT[sid] && activeStreamId && typeof loadInflightState === 'function') {
    const stored = loadInflightState(sid, activeStreamId);
    if (stored) {
      INFLIGHT[sid] = {
        messages: Array.isArray((stored as { messages?: unknown[] }).messages) && ((stored as { messages: unknown[] }).messages).length
          ? (stored as { messages: unknown[] }).messages as Message[]
          : [...((data.session.messages || []) as Message[])],
        uploaded: Array.isArray((stored as { uploaded?: unknown[] }).uploaded)
          ? (stored as { uploaded: unknown[] }).uploaded
          : [...((data.session.pending_attachments || []) as unknown[])],
        toolCalls: Array.isArray((stored as { toolCalls?: unknown[] }).toolCalls)
          ? (stored as { toolCalls: unknown[] }).toolCalls
          : [],
        reattach: true,
      };
    }
  }
  if (INFLIGHT[sid]) {
    S.messages = INFLIGHT[sid].messages;
    S.toolCalls = (INFLIGHT[sid].toolCalls || []) as unknown[];
    S.busy = true;
    syncTopbar(); renderMessages(); appendThinking(); loadDir('.');
    clearLiveToolCards();
    if (typeof placeLiveToolCardsHost === 'function') placeLiveToolCardsHost();
    for (const tc of (S.toolCalls || [])) {
      if (tc && (tc as { name?: string }).name) appendLiveToolCard(tc);
    }
    setBusy(true); setComposerStatus('');
    startApprovalPolling(sid);
    if (typeof startClarifyPolling === 'function') startClarifyPolling(sid);
    S.activeStreamId = activeStreamId;
    const _cb = $('btnCancel'); if (_cb && activeStreamId) _cb.style.display = 'inline-flex';
    if (INFLIGHT[sid].reattach && activeStreamId && typeof attachLiveStream === 'function') {
      INFLIGHT[sid].reattach = false;
      attachLiveStream(sid, activeStreamId, (data.session.pending_attachments || []) as unknown[], { reconnecting: true });
    }
  } else {
    updateQueueBadge(sid);
    S.messages = (data.session.messages || []) as Message[];
    const pendingMsg = typeof getPendingSessionMessage === 'function' ? getPendingSessionMessage(data.session) as Message | null : null;
    if (pendingMsg) S.messages.push(pendingMsg);
    if (!hasMessageToolMetadata && data.session.tool_calls && data.session.tool_calls.length) {
      S.toolCalls = ((data.session.tool_calls || []) as unknown[]).map((tc: unknown) => ({ ...(tc as object), done: true }));
    } else {
      S.toolCalls = [];
    }
    clearLiveToolCards();
    if (activeStreamId) {
      S.busy = true;
      S.activeStreamId = activeStreamId;
      updateSendBtn();
      const _cb = $('btnCancel'); if (_cb) _cb.style.display = 'inline-flex';
      setStatus('');
      setComposerStatus('');
      syncTopbar(); renderMessages(); appendThinking(); loadDir('.');
      updateQueueBadge(sid);
      if((data.session as Record<string,unknown>).has_pending_approvals) startApprovalPolling(sid);
      if (typeof startClarifyPolling === 'function') startClarifyPolling(sid);
      if (typeof attachLiveStream === 'function') attachLiveStream(sid, activeStreamId, (data.session.pending_attachments || []) as unknown[], { reconnecting: true });
      else if (typeof watchInflightSession === 'function') watchInflightSession(sid, activeStreamId);
    } else {
      S.busy = false;
      S.activeStreamId = null;
      updateSendBtn();
      const _cb = $('btnCancel'); if (_cb) _cb.style.display = 'none';
      setStatus('');
      setComposerStatus('');
      updateQueueBadge(sid);
      syncTopbar(); renderMessages(); highlightCode!(); loadDir('.');
    }
  }
  const _s = S.session;
  if (_s && typeof _syncCtxIndicator === 'function') {
    const u = S.lastUsage || {};
    const sRec = _s as unknown as Record<string, unknown>;
    const uRec = u as Record<string, unknown>;
    _syncCtxIndicator({
      input_tokens:       (uRec.input_tokens ?? sRec.input_tokens ?? 0) as number,
      output_tokens:      (uRec.output_tokens ?? sRec.output_tokens ?? 0) as number,
      estimated_cost:    (uRec.estimated_cost ?? sRec.estimated_cost ?? 0) as number,
      context_length:    (uRec.context_length ?? sRec.context_length ?? 0) as number,
      last_prompt_tokens:(uRec.last_prompt_tokens ?? sRec.last_prompt_tokens ?? 0) as number,
      threshold_tokens:  (uRec.threshold_tokens ?? sRec.threshold_tokens ?? 0) as number,
    });
  }
}

let _allSessions: Session[] = [];
let _renamingSid: string | null = null;
let _showArchived = false;
let _allProjects: Array<{ project_id: string; name: string; color?: string }> = [];
let _activeProject: string | null = null;
let _showAllProfiles = false;
let _sessionActionMenu: HTMLDivElement | null = null;
let _sessionActionAnchor: HTMLButtonElement | null = null;
let _sessionActionSessionId: string | null = null;
let _folderCollapsed: Record<string, boolean> = {};
let _activeTagFolder: string | null = null;

// ── Drag state ────────────────────────────────────────────────────────────────
let _dragSessionId: string | null = null;
let _dragOverFolderId: string | null = null;

function closeSessionActionMenu(): void {
  if (_sessionActionMenu) { _sessionActionMenu.remove(); _sessionActionMenu = null; }
  if (_sessionActionAnchor) {
    _sessionActionAnchor.classList.remove('active');
    const row = _sessionActionAnchor.closest('.session-item');
    if (row) row.classList.remove('menu-open');
    _sessionActionAnchor = null;
  }
  _sessionActionSessionId = null;
}

function _positionSessionActionMenu(anchorEl: HTMLElement): void {
  if (!_sessionActionMenu || !anchorEl) return;
  const rect = anchorEl.getBoundingClientRect();
  const menuW = Math.min(280, Math.max(220, _sessionActionMenu.scrollWidth || 220));
  let left = rect.right - menuW;
  if (left < 8) left = 8;
  if (left + menuW > window.innerWidth - 8) left = window.innerWidth - menuW - 8;
  _sessionActionMenu.style.left = left + 'px';
  _sessionActionMenu.style.top = '8px';
  const menuH = _sessionActionMenu.offsetHeight || 0;
  let top = rect.bottom + 6;
  if (top + menuH > window.innerHeight - 8 && rect.top > menuH + 12) top = rect.top - menuH - 6;
  if (top < 8) top = 8;
  _sessionActionMenu.style.top = top + 'px';
}

function _buildSessionAction(
  label: string, meta: string, icon: string,
  onSelect: () => Promise<void>, extraClass = ''
): HTMLButtonElement {
  const opt = document.createElement('button');
  opt.type = 'button';
  opt.className = 'ws-opt session-action-opt' + (extraClass ? ` ${extraClass}` : '');
  opt.innerHTML =
    `<span class="ws-opt-action">`
    + `<span class="ws-opt-icon">${icon}</span>`
    + `<span class="session-action-copy">`
      + `<span class="ws-opt-name">${esc(label)}</span>`
      + (meta ? `<span class="session-action-meta">${esc(meta)}</span>` : '')
    + `</span>`
  + `</span>`;
  opt.onclick = async(e) => {
    e.stopPropagation();
    closeSessionActionMenu();
    await onSelect();
  };
  return opt;
}

function _openSessionActionMenu(session: Session, anchorEl: HTMLButtonElement): void {
  closeSessionActionMenu();
  const menu = document.createElement('div');
  menu.className = 'session-action-menu ws-menu';
  menu.setAttribute('role', 'menu');
  menu.setAttribute('aria-label', 'Session actions');

  // Pin / Unpin
  const newPinned = !session.pinned;
  menu.appendChild(_buildSessionAction(
    session.pinned ? 'Unpin' : 'Pin',
    session.pinned ? 'Remove from pinned' : 'Pin to top',
    session.pinned ? ICONS.unpin : ICONS.pin,
    async() => {
      try {
        await api('/api/session/pin', { method: 'POST', body: JSON.stringify({ session_id: session.session_id, pinned: newPinned }) });
        session.pinned = newPinned;
        if (S.session && S.session.session_id === session.session_id) S.session.pinned = newPinned;
        renderSessionList();
      } catch (err: unknown) { showToast('Pin failed: ' + ((err as Error).message || String(err))); }
    }
  ));

  // Archive / Unarchive
  menu.appendChild(_buildSessionAction(
    session.archived ? 'Restore' : 'Archive',
    session.archived ? 'Restore to inbox' : 'Remove from list',
    session.archived ? ICONS.unarchive : ICONS.archive,
    async() => {
      try {
        await api('/api/session/archive', { method: 'POST', body: JSON.stringify({ session_id: session.session_id, archived: !session.archived }) });
        session.archived = !session.archived;
        if (S.session && S.session.session_id === session.session_id) S.session.archived = session.archived;
        await renderSessionList();
        showToast(session.archived ? 'Session archived' : 'Session restored');
      } catch (err: unknown) { showToast('Archive failed: ' + ((err as Error).message || String(err))); }
    }
  ));

  // Duplicate
  menu.appendChild(_buildSessionAction(
    'Duplicate', 'Create a copy', ICONS.dup,
    async() => {
      try {
        const res = await api('/api/session/new', { method: 'POST', body: JSON.stringify({ clone_from: session.session_id }) }) as { session?: Session };
        if (res.session) {
          await api('/api/session/rename', { method: 'POST', body: JSON.stringify({ session_id: res.session.session_id, title: (session.title || 'Untitled') + ' (copy)' }) });
          await loadSession(res.session.session_id);
          await renderSessionList();
          showToast('Session duplicated');
        }
      } catch (err: unknown) { showToast('Duplicate failed: ' + ((err as Error).message || String(err))); }
    }
  ));

  // Move to project
  const currentProj = session.project_id ? _allProjects.find(p => p.project_id === session.project_id) : null;
  menu.appendChild(_buildSessionAction(
    'Move to project', currentProj ? `Currently: ${currentProj.name}` : 'Assign to project', ICONS.folder,
    async() => { _showProjectPicker(session, anchorEl); }
  ));

  // Delete
  menu.appendChild(_buildSessionAction(
    'Delete', 'Permanently remove', ICONS.trash,
    async() => { await deleteSession(session.session_id); },
    'danger'
  ));

  _sessionActionMenu = menu;
  _sessionActionAnchor = anchorEl;
  _sessionActionSessionId = session.session_id;
  anchorEl.classList.add('active');
  anchorEl.closest('.session-item')?.classList.add('menu-open');
  document.body.appendChild(menu);
  if (_sessionActionMenu && _sessionActionAnchor) _positionSessionActionMenu(_sessionActionAnchor);
}

document.addEventListener('click', (e) => {
  if (_sessionActionMenu && !_sessionActionMenu.contains(e.target as Node)) closeSessionActionMenu();
  if (_sessionActionMenu && _sessionActionAnchor) _positionSessionActionMenu(_sessionActionAnchor);
});

async function renderSessionList(): Promise<void> {
  try {
    if (!($('sessionSearch') as HTMLInputElement | null)?.value?.trim()) _contentSearchResults = [];
    const [sessData, projData] = await Promise.all([
      api('/api/sessions') as Promise<{ sessions?: Session[] }>,
      api('/api/projects') as Promise<{ projects?: Array<{ project_id: string; name: string; color?: string }> }>,
    ]);
    _allSessions = sessData.sessions || [];
    _allProjects = (projData.projects || []).map((p: any) => ({ ...p, project_id: p.project_id || p.id }));
    renderSessionListFromCache();
  } catch (e: unknown) { console.warn('renderSessionList', e); }
}

// ── Gateway session SSE (real-time sync for agent sessions) ──
let _gatewaySSE: EventSource | null = null;
let _gatewaySSEConnected = false;

function startGatewaySSE(): void {
  if (_gatewaySSE) return;
  _gatewaySSE = new EventSource('api/sessions/gateway/stream');
  _gatewaySSE.addEventListener('sessions_changed', (ev) => {
    try {
      const data = JSON.parse(ev.data) as { sessions?: Array<{ session_id: string }> };
      if (data.sessions) {
        renderSessionList();
        if (S.session && !S.busy && S.session.is_cli_session) {
          const changedIds = new Set((data.sessions || []).map(s => s.session_id));
          if (changedIds.has(S.session.session_id)) {
            loadSession(S.session.session_id);
          }
        }
      }
    } catch { /* ignore */ }
  });
  _gatewaySSE.onerror = () => {
    _gatewaySSE?.close();
    _gatewaySSE = null;
    _gatewaySSEConnected = false;
    setTimeout(startGatewaySSE, 5000);
  };
  _gatewaySSE.onopen = () => { _gatewaySSEConnected = true; };
}

function stopGatewaySSE(): void {
  _gatewaySSE?.close();
  _gatewaySSE = null;
  _gatewaySSEConnected = false;
}

let _contentSearchResults: Session[] = [];

function filterSessions(): void {
  renderSessionListFromCache();
  const q = ($('sessionSearch') as HTMLInputElement | null)?.value?.trim() || '';
  clearTimeout(_searchDebounceTimer ?? undefined);
  if (!q) { _contentSearchResults = []; return; }
  _searchDebounceTimer = setTimeout(async () => {
    const depth = 5;
    const q2 = ($('sessionSearch') as HTMLInputElement | null)?.value?.trim() || '';
    if (!q2) return;
    try {
      const data = await api(`/api/sessions/search?q=${encodeURIComponent(q2)}&content=1&depth=${depth}`) as { sessions?: Session[] };
      _contentSearchResults = ((data.sessions || []) as Session[]).filter(s => (s as { match_type?: string }).match_type === 'content' && !titleIds.has(s.session_id));
      renderSessionListFromCache();
    } catch { /* ignore */ }
  }, 300) as unknown as number;
}
let _searchDebounceTimer: number | null = null;
let titleIds: Set<string> = new Set();

function _sessionTimestampMs(s: Session): number {
  return (s as Record<string, unknown>).last_message_at as number
    || (s as Record<string, unknown>).updated_at as number
    || (s as Record<string, unknown>).created_at as number
    || Date.now();
}

function _localDayOrdinal(timestampMs: number): number {
  const date = new Date(timestampMs);
  return Math.floor(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / 86400000);
}

function _sessionCalendarBoundaries(nowMs: number): { startOfToday: number; startOfYesterday: number; startOfWeek: number; startOfLastWeek: number } {
  const now = new Date(nowMs);
  const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const startOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
  const startOfWeek = new Date(startOfToday);
  startOfWeek.setDate(startOfWeek.getDate() - ((startOfWeek.getDay() + 6) % 7));
  const startOfLastWeek = new Date(startOfWeek);
  startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
  return {
    startOfToday: startOfToday.getTime(),
    startOfYesterday: startOfYesterday.getTime(),
    startOfWeek: startOfWeek.getTime(),
    startOfLastWeek: startOfLastWeek.getTime(),
  };
}

function _sessionTimeBucketLabel(ts: number, now: number): string {
  if (!ts) return 'Older';
  const { startOfToday, startOfYesterday, startOfWeek, startOfLastWeek } = _sessionCalendarBoundaries(now);
  if (ts >= startOfToday) return 'Today';
  if (ts >= startOfYesterday) return 'Yesterday';
  if (ts >= startOfWeek) return 'This week';
  if (ts >= startOfLastWeek) return 'Last week';
  return 'Older';
}

function _sessionTimestampLabel(s: Session): string {
  return _formatTs(_sessionTimestampMs(s));
}

function _formatTs(ts: number): string {
  const d = new Date(ts);
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const yesterday = new Date(today.getTime() - 86400000);
  const targetDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  if (targetDay >= today) {
    return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
  }
  if (targetDay >= yesterday) {
    return 'Yesterday';
  }
  return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}

function _renderOneSession(s: Session): HTMLElement {
  const el = document.createElement('div');
  el.className = 'session-item' + (s.session_id === S.session?.session_id ? ' active' : '');
  if (s.archived) el.classList.add('archived');
  if (s.pinned) el.classList.add('pinned');
  if (s.is_cli_session) el.classList.add('cli-session');
  el.setAttribute('data-session-id', s.session_id);
  el.setAttribute('draggable', 'true');
  el.setAttribute('role', 'button');
  el.setAttribute('tabindex', '0');

  // ── Drag handlers ──────────────────────────────────────────────────────
  el.addEventListener('dragstart', (e) => {
    _dragSessionId = s.session_id;
    el.classList.add('dragging');
    e.dataTransfer!.effectAllowed = 'move';
    e.dataTransfer!.setData('text/plain', s.session_id);
  });
  el.addEventListener('dragend', () => {
    _dragSessionId = null;
    _dragOverFolderId = null;
    el.classList.remove('dragging');
    document.querySelectorAll('.project-folder.drag-over, .tag-folder.drag-over').forEach(f => f.classList.remove('drag-over'));
  });
  el.addEventListener('dragover', (e) => {
    if (!_dragSessionId || _dragSessionId === s.session_id) return;
    e.preventDefault();
    e.dataTransfer!.dropEffect = 'move';
  });
  el.addEventListener('drop', async(e) => {
    if (!_dragSessionId || _dragSessionId === s.session_id) return;
    e.preventDefault();
    // Dropping on a session item = move to that session's project (if any)
    const targetSession = s;
    if (targetSession.project_id) {
      await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: _dragSessionId, project_id: targetSession.project_id }) });
      showToast('Moved to ' + (_allProjects.find(p => p.project_id === targetSession.project_id)?.name || 'project'));
    } else {
      await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: _dragSessionId, project_id: null }) });
      showToast('Removed from project');
    }
    _dragSessionId = null;
    _dragOverFolderId = null;
    await renderSessionList();
  });

  const sessionText = document.createElement('div');
  sessionText.className = 'session-text';

  const title = document.createElement('div');
  title.className = 'session-title';

  const titleInner = document.createElement('span');
  titleInner.className = 'session-title-text';
  titleInner.textContent = s.title || 'Untitled';
  title.appendChild(titleInner);

  const meta = document.createElement('span');
  meta.className = 'session-meta';
  meta.textContent = _sessionTimestampLabel(s);
  title.appendChild(meta);

  const startRename = () => {
    if (_renamingSid) return;
    _renamingSid = s.session_id;
    const inp = document.createElement('input');
    inp.type = 'text';
    inp.className = 'file-rename-input session-rename-input';
    inp.value = s.title || 'Untitled';
    inp.oncompositionstart = () => { (inp as any)._composing = true; };
    inp.oncompositionend = () => { (inp as any)._composing = false; };
    const finish = async(save: boolean): Promise<void> => {
      _renamingSid = null;
      if (save && inp.value.trim() && inp.value.trim() !== (s.title || 'Untitled')) {
        const newTitle = inp.value.trim();
        s.title = newTitle;
        if (S.session && S.session.session_id === s.session_id) { S.session.title = newTitle; syncTopbar(); }
        try { await api('/api/session/rename', { method: 'POST', body: JSON.stringify({ session_id: s.session_id, title: newTitle }) }); }
        catch (err: unknown) { setStatus('Rename failed: ' + ((err as Error).message || String(err))); }
      }
      inp.replaceWith(title);
      setTimeout(() => { if (_renamingSid === null) renderSessionListFromCache(); }, 50);
    };
    inp.onkeydown = e2 => {
      if (e2.key === 'Enter') { if (e2.isComposing) return; e2.preventDefault(); e2.stopPropagation(); finish(true); }
      if (e2.key === 'Escape') { e2.preventDefault(); e2.stopPropagation(); finish(false); }
    };
    inp.onblur = () => { if (_renamingSid === s.session_id) finish(false); };
    title.replaceWith(inp);
    setTimeout(() => { inp.focus(); inp.select(); }, 10);
  };

  if (s.pinned) {
    const pinInd = document.createElement('span');
    pinInd.className = 'session-pin-indicator';
    pinInd.innerHTML = ICONS.pin;
    el.appendChild(pinInd);
  }
  if (s.project_id) {
    const proj = _allProjects.find(p => p.project_id === s.project_id);
    if (proj) {
      const folderIcon = document.createElement('span');
      folderIcon.innerHTML = ICONS.folder;
      folderIcon.className = 'session-project-folder';
      folderIcon.title = proj.name;
      title.appendChild(folderIcon);
      const dot = document.createElement('span');
      dot.className = 'session-project-dot';
      dot.style.background = proj.color || 'var(--blue)';
      dot.title = proj.name;
      title.appendChild(dot);
    }
  }
  el.appendChild(sessionText);
  sessionText.appendChild(title);
  const actions = document.createElement('div');
  actions.className = 'session-actions';
  const menuBtn = document.createElement('button');
  menuBtn.type = 'button';
  menuBtn.className = 'session-actions-trigger';
  menuBtn.title = 'Conversation actions';
  menuBtn.setAttribute('aria-haspopup', 'menu');
  menuBtn.setAttribute('aria-label', 'Conversation actions');
  menuBtn.innerHTML = ICONS.more;
  menuBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); _openSessionActionMenu(s, menuBtn); };
  actions.appendChild(menuBtn);
  el.appendChild(actions);

  let _clickTimer: ReturnType<typeof setTimeout> | null = null;
  el.onclick = async(e): Promise<void> => {
    if (_renamingSid) return;
    if (actions.contains(e.target as Node)) return;
    clearTimeout(_clickTimer ?? undefined);
    _clickTimer = setTimeout(async() => {
      _clickTimer = null;
      if (_renamingSid) return;
      if (s.is_cli_session) {
        try { await api('/api/session/import_cli', { method: 'POST', body: JSON.stringify({ session_id: s.session_id }) }); }
        catch { /* import failed -- fall through to read-only view */ }
      }
      await loadSession(s.session_id); renderSessionListFromCache();
      if (typeof closeMobileSidebar === 'function') closeMobileSidebar();
    }, 300);
  };
  el.ondblclick = (e): void => {
    e.stopPropagation(); e.preventDefault();
    clearTimeout(_clickTimer ?? undefined); _clickTimer = null;
    startRename();
  };
  return el;
}

async function deleteSession(sid: string): Promise<void> {
  const ok = await showConfirmDialog({
    message: 'Delete this conversation?',
    confirmLabel: t('delete_title'),
    danger: true
  });
  if (!ok) return;
  try {
    await api('/api/session/delete', { method: 'POST', body: JSON.stringify({ session_id: sid }) });
  } catch (err: unknown) { setStatus(`Delete failed: ${(err as Error).message || String(err)}`); return; }
  if (typeof INFLIGHT !== 'undefined' && sid in INFLIGHT) delete INFLIGHT[sid];
  if (typeof clearInflightState === 'function') clearInflightState(sid);
  if (typeof clearInflight === 'function') clearInflight();
  if (S.session && S.session.session_id === sid) {
    S.session = null; S.messages = []; S.entries = [];
    localStorage.removeItem('hermes-webui-session');
    const remaining = await api('/api/sessions') as { sessions?: Session[] };
    if (remaining.sessions && remaining.sessions.length) {
      await loadSession(remaining.sessions[0].session_id);
    } else {
      const topbarTitle = $('topbarTitle');
      const topbarMeta = $('topbarMeta');
      const msgInner = $('msgInner');
      const emptyState = $('emptyState');
      const fileTree = $('fileTree');
      if (topbarTitle) topbarTitle.textContent = window._botName || 'Hermes';
      if (topbarMeta) topbarMeta.textContent = 'Start a new conversation';
      if (msgInner) msgInner.innerHTML = '';
      if (emptyState) emptyState.style.display = '';
      if (fileTree) fileTree.innerHTML = '';
    }
  }
  showToast('Conversation deleted');
  await renderSessionList();
}

// ── Project helpers ────────────────────────────────────────────────────────────
const PROJECT_COLORS = ['#7cb9ff', '#f5c542', '#e94560', '#50c878', '#c084fc', '#fb923c', '#67e8f9', '#f472b6'];

function _showProjectPicker(session: Session, anchorEl: HTMLElement): void {
  document.querySelectorAll('.project-picker').forEach(p => (p as any).remove());
  const picker = document.createElement('div');
  picker.className = 'project-picker';
  const none = document.createElement('div');
  none.className = 'project-picker-item' + (!session.project_id ? ' active' : '');
  none.textContent = 'No project';
  none.onclick = async(): Promise<void> => {
    picker.remove();
    document.removeEventListener('click', close as EventListener);
    await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: session.session_id, project_id: null }) });
    session.project_id = null;
    renderSessionListFromCache();
    showToast('Removed from project');
  };
  picker.appendChild(none);
  for (const p of _allProjects) {
    const item = document.createElement('div');
    item.className = 'project-picker-item' + (session.project_id === p.project_id ? ' active' : '');
    if (p.color) {
      const dot = document.createElement('span');
      dot.className = 'color-dot';
      dot.style.cssText = 'width:6px;height:6px;border-radius:50%;background:' + p.color + ';flex-shrink:0;';
      item.appendChild(dot);
    }
    const name = document.createElement('span');
    name.textContent = p.name;
    item.appendChild(name);
    item.onclick = async(): Promise<void> => {
      picker.remove();
      document.removeEventListener('click', close as EventListener);
      await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: session.session_id, project_id: p.project_id }) });
      session.project_id = p.project_id;
      renderSessionListFromCache();
      showToast('Moved to ' + p.name);
    };
    picker.appendChild(item);
  }
  const createItem = document.createElement('div');
  createItem.className = 'project-picker-item project-picker-create';
  createItem.textContent = '+ New project';
  createItem.onclick = async(): Promise<void> => {
    picker.remove();
    document.removeEventListener('click', close as EventListener);
    const name = await showPromptDialog({ message: t('project_name_prompt'), confirmLabel: t('create'), placeholder: 'Project name' });
    if (!name || !(name as string).trim()) return;
    const color = PROJECT_COLORS[_allProjects.length % PROJECT_COLORS.length];
    const res = await api('/api/projects/create', { method: 'POST', body: JSON.stringify({ name: (name as string).trim(), color }) }) as { project?: { project_id: string; name: string } };
    if (res.project) {
      _allProjects.push(res.project);
      await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: session.session_id, project_id: res.project.project_id }) });
      session.project_id = res.project.project_id;
      await renderSessionList();
      showToast('Created "' + res.project.name + '" and moved session');
    }
  };
  picker.appendChild(createItem);
  document.body.appendChild(picker);
  const rect = anchorEl.getBoundingClientRect();
  picker.style.position = 'fixed';
  picker.style.zIndex = '999';
  const spaceBelow = window.innerHeight - rect.bottom;
  if (spaceBelow < 160 && rect.top > 160) {
    picker.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
    picker.style.top = 'auto';
  } else {
    picker.style.top = (rect.bottom + 4) + 'px';
    picker.style.bottom = 'auto';
  }
  const pickerW = Math.min(220, Math.max(160, picker.scrollWidth || 160));
  let left = rect.right - pickerW;
  if (left < 8) left = 8;
  picker.style.left = left + 'px';
  const close = (e: MouseEvent): void => { if (!picker.contains(e.target as Node) && e.target !== anchorEl) { picker.remove(); document.removeEventListener('click', close as EventListener); } };
  setTimeout(() => document.addEventListener('click', close as EventListener), 0);
}

function _startProjectCreate(bar: HTMLElement, addBtn: HTMLButtonElement): void {
  const inp = document.createElement('input');
  inp.className = 'project-create-input';
  inp.placeholder = 'Project name';
  const finish = async(save: boolean): Promise<void> => {
    if (save && inp.value.trim()) {
      const color = PROJECT_COLORS[_allProjects.length % PROJECT_COLORS.length];
      await api('/api/projects/create', { method: 'POST', body: JSON.stringify({ name: inp.value.trim(), color }) });
      await renderSessionList();
      showToast('Project created');
    } else {
      inp.replaceWith(addBtn);
    }
  };
  inp.onkeydown = (e): void => {
    if (e.key === 'Enter') { if (e.isComposing) return; e.preventDefault(); finish(true); }
    if (e.key === 'Escape') { e.preventDefault(); finish(false); }
  };
  inp.onblur = () => finish(false);
  addBtn.replaceWith(inp);
  setTimeout(() => inp.focus(), 10);
}

function _startProjectRename(proj: { project_id: string; name: string }, chip: HTMLElement): void {
  const inp = document.createElement('input');
  inp.className = 'project-create-input';
  inp.value = proj.name;
  const finish = async(save: boolean): Promise<void> => {
    if (save && inp.value.trim() && inp.value.trim() !== proj.name) {
      await api('/api/projects/rename', { method: 'POST', body: JSON.stringify({ project_id: proj.project_id, name: inp.value.trim() }) });
      await renderSessionList();
      showToast('Project renamed');
    } else {
      renderSessionListFromCache();
    }
  };
  inp.onkeydown = (e): void => {
    if (e.key === 'Enter') { if (e.isComposing) return; e.preventDefault(); finish(true); }
    if (e.key === 'Escape') { e.preventDefault(); finish(false); }
  };
  inp.onblur = (): void => { if ((inp as HTMLInputElement).disabled) return; finish(false); };
  inp.onclick = (e) => e.stopPropagation();
  chip.replaceWith(inp);
  setTimeout(() => { inp.focus(); inp.select(); }, 10);
}

async function _confirmDeleteProject(proj: { project_id: string; name: string }): Promise<void> {
  const ok = await showConfirmDialog({ message: 'Delete project "' + proj.name + '"? Sessions will be unassigned but not deleted.', confirmLabel: t('delete_title'), danger: true });
  if (!ok) return;
  await api('/api/projects/delete', { method: 'POST', body: JSON.stringify({ project_id: proj.project_id }) });
  if (_activeProject === proj.project_id) _activeProject = null;
  await renderSessionList();
  showToast('Project deleted');
}

// ── Render session list from cache ─────────────────────────────────────────────
function renderSessionListFromCache(): void {
  closeSessionActionMenu();
  const q = (($('sessionSearch') as HTMLInputElement | null)?.value || '').toLowerCase();
  const titleMatches = q ? _allSessions.filter(s => (s.title || 'Untitled').toLowerCase().includes(q)) : _allSessions;
  titleIds = new Set(titleMatches.map(s => s.session_id));
  const allMatched = q ? [...titleMatches, ..._contentSearchResults.filter(s => !titleIds.has(s.session_id))] : titleMatches;
  const profileFiltered = _showAllProfiles ? allMatched : allMatched.filter(s => s.is_cli_session || s.profile === S.activeProfile);
  const projectFiltered = _activeProject
    ? profileFiltered.filter(s => s.project_id === _activeProject)
    : profileFiltered;
  const sessions = _showArchived ? projectFiltered : projectFiltered.filter(s => !s.archived);
  const archivedCount = projectFiltered.filter(s => s.archived).length;
  const list = $('sessionList') as HTMLElement | null; if (!list) return;
  list.innerHTML = '';

  // ── Load folder collapse state ─────────────────────────────────────────────
  try { _folderCollapsed = JSON.parse(localStorage.getItem('hermes-folders-collapsed') || '{}'); } catch { }
  const _saveFolderCollapsed = () => { try { localStorage.setItem('hermes-folders-collapsed', JSON.stringify(_folderCollapsed)); } catch { } };

  // ── Time-based session grouping (replaces tag-based) ──────────────────────────
  // All sessions visible: pinned first, then by time bucket (no project filter hiding sessions)
  const sessionsNoProject = profileFiltered.filter(s => !s.project_id);
  const pinnedSessions = sessionsNoProject.filter(s => s.pinned).sort((a, b) => _sessionTimestampMs(b) - _sessionTimestampMs(a));
  const unpinnedSessions = sessionsNoProject.filter(s => !s.pinned).sort((a, b) => _sessionTimestampMs(b) - _sessionTimestampMs(a));

  // Time buckets
  const nowMs = Date.now();
  const { startOfToday, startOfYesterday, startOfWeek, startOfLastWeek } = _sessionCalendarBoundaries(nowMs);
  const buckets: { label: string; ids: Set<string> }[] = [
    { label: 'Today', ids: new Set(unpinnedSessions.filter(s => _sessionTimestampMs(s) >= startOfToday).map(s => s.session_id)) },
    { label: 'Yesterday', ids: new Set(unpinnedSessions.filter(s => _sessionTimestampMs(s) >= startOfYesterday && _sessionTimestampMs(s) < startOfToday).map(s => s.session_id)) },
    { label: 'This week', ids: new Set(unpinnedSessions.filter(s => _sessionTimestampMs(s) >= startOfWeek && _sessionTimestampMs(s) < startOfYesterday).map(s => s.session_id)) },
    { label: 'Last week', ids: new Set(unpinnedSessions.filter(s => _sessionTimestampMs(s) >= startOfLastWeek && _sessionTimestampMs(s) < startOfWeek).map(s => s.session_id)) },
    { label: 'Older', ids: new Set(unpinnedSessions.filter(s => _sessionTimestampMs(s) < startOfLastWeek).map(s => s.session_id)) },
  ];

  // Pinned section
  if (pinnedSessions.length > 0) {
    const pinnedSection = document.createElement('div');
    pinnedSection.className = 'project-folders-section';

    const folder = document.createElement('div');
    folder.className = 'project-folder';

    const hdr = document.createElement('div');
    hdr.className = 'project-folder-header';

    const caret = document.createElement('span');
    caret.className = 'project-folder-caret';
    caret.textContent = '\u25B8';

    const dot = document.createElement('span');
    dot.className = 'project-folder-dot';
    dot.style.background = '#fbbf24';

    const label = document.createElement('span');
    label.className = 'project-folder-label';
    label.textContent = 'Pinned';

    const count = document.createElement('span');
    count.className = 'project-folder-count';
    count.textContent = String(pinnedSessions.length);

    hdr.appendChild(caret); hdr.appendChild(dot); hdr.appendChild(label); hdr.appendChild(count);

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

    for (const s of pinnedSessions) body.appendChild(_renderOneSession(s));
    folder.appendChild(hdr);
    folder.appendChild(body);
    pinnedSection.appendChild(folder);
    list.appendChild(pinnedSection);
  }

  // Time bucket sections
  for (const bucket of buckets) {
    const bucketSessions = unpinnedSessions.filter(s => bucket.ids.has(s.session_id));
    if (bucketSessions.length === 0) continue;

    const isCollapsed = _folderCollapsed['time:' + bucket.label] || false;
    const section = document.createElement('div');
    section.className = 'project-folders-section';

    const folder = document.createElement('div');
    folder.className = 'project-folder';

    const hdr = document.createElement('div');
    hdr.className = 'project-folder-header';

    const caret = document.createElement('span');
    caret.className = 'project-folder-caret' + (isCollapsed ? ' collapsed' : '');
    caret.textContent = '\u25B8';

    const dot = document.createElement('span');
    dot.className = 'project-folder-dot';
    dot.style.background = bucket.label === 'Today' ? '#4ade80' : bucket.label === 'Yesterday' ? '#60a5fa' : '#a78bfa';

    const label = document.createElement('span');
    label.className = 'project-folder-label';
    label.textContent = bucket.label;

    const count = document.createElement('span');
    count.className = 'project-folder-count';
    count.textContent = String(bucketSessions.length);

    hdr.appendChild(caret); hdr.appendChild(dot); hdr.appendChild(label); hdr.appendChild(count);
    hdr.onclick = () => {
      const collapsed = body.style.display === 'none';
      body.style.display = collapsed ? '' : 'none';
      caret.classList.toggle('collapsed', !collapsed);
      _folderCollapsed['time:' + bucket.label] = !collapsed;
      _saveFolderCollapsed();
    };

    const body = document.createElement('div');
    body.className = 'tag-folder-body';
    if (isCollapsed) body.style.display = 'none';

    for (const s of bucketSessions) body.appendChild(_renderOneSession(s));
    folder.appendChild(hdr);
    folder.appendChild(body);
    section.appendChild(folder);
    list.appendChild(section);
  }

  // ── Project folders section (sessions WITH project_id) ─────────────────────────
  const sessionsWithProject = profileFiltered.filter(s => s.project_id);
  if (sessionsWithProject.length > 0 && _allProjects.length > 0) {
    const projSection = document.createElement('div');
    projSection.className = 'project-folders-section';

    for (const p of _allProjects) {
      const projSessions = sessionsWithProject.filter(s => s.project_id === p.project_id);
      if (projSessions.length === 0) continue;

      const isCollapsed = _folderCollapsed['proj:' + p.project_id] || false;
      const folder = document.createElement('div');
      folder.className = 'project-folder' + (p.project_id === _activeProject ? ' active' : '');
      folder.setAttribute('data-project-id', p.project_id);

      const hdr = document.createElement('div');
      hdr.className = 'project-folder-header';

      const caret = document.createElement('span');
      caret.className = 'project-folder-caret' + (isCollapsed ? ' collapsed' : '');
      caret.textContent = '\u25B8';

      const dot = document.createElement('span');
      dot.className = 'project-folder-dot';
      dot.style.background = p.color || 'var(--blue)';

      const label = document.createElement('span');
      label.className = 'project-folder-label';
      label.textContent = p.name;

      const count = document.createElement('span');
      count.className = 'project-folder-count';
      count.textContent = String(projSessions.length);

      hdr.appendChild(caret); hdr.appendChild(dot); hdr.appendChild(label); hdr.appendChild(count);

      const body = document.createElement('div');
      body.className = 'tag-folder-body';
      if (isCollapsed) body.style.display = 'none';

      if (p.project_id === _activeProject) {
        const pinned = projSessions.filter(s => s.pinned).sort((a, b) => _sessionTimestampMs(b) - _sessionTimestampMs(a));
        const unpinned = projSessions.filter(s => !s.pinned).sort((a, b) => _sessionTimestampMs(b) - _sessionTimestampMs(a));
        for (const s of pinned) body.appendChild(_renderOneSession(s));
        for (const s of unpinned) body.appendChild(_renderOneSession(s));
      }

      hdr.onclick = () => {
        if (p.project_id === _activeProject) {
          _activeProject = null;
        } else {
          _activeProject = p.project_id;
        }
        renderSessionListFromCache();
      };

      folder.appendChild(hdr);
      folder.appendChild(body);

      folder.addEventListener('dragover', (e) => {
        if (!_dragSessionId) return;
        e.preventDefault();
        folder.classList.add('drag-over');
      });
      folder.addEventListener('dragleave', () => folder.classList.remove('drag-over'));
      folder.addEventListener('drop', async(e) => {
        if (!_dragSessionId) return;
        e.preventDefault();
        folder.classList.remove('drag-over');
        await api('/api/session/move', { method: 'POST', body: JSON.stringify({ session_id: _dragSessionId, project_id: p.project_id }) });
        showToast('Moved to ' + p.name);
        _dragSessionId = null;
        await renderSessionList();
      });

      projSection.appendChild(folder);
    }

    list.appendChild(projSection);
  }

  // ── New Project button ────────────────────────────────────────────────────────
  if (_allProjects.length >= 0) {
    const addSection = document.createElement('div');
    addSection.className = 'project-folders-section';
    const folder = document.createElement('div');
    folder.className = 'project-folder project-folder-add';
    const hdr = document.createElement('div');
    hdr.className = 'project-folder-header project-folder-add-header';
    const caret = document.createElement('span');
    caret.style.cssText = 'opacity:0.4;';
    const dot = document.createElement('span');
    dot.style.cssText = 'width:8px;height:8px;border-radius:50%;border:1.5px dashed rgba(255,255,255,.3);display:inline-block;';
    const label = document.createElement('span');
    label.className = 'project-folder-label';
    label.style.cssText = 'opacity:0.5;';
    label.textContent = 'New Project';
    hdr.appendChild(caret); hdr.appendChild(dot); hdr.appendChild(label);
    hdr.onclick = (e) => { e.stopPropagation(); _startProjectCreate(list, hdr as any); };
    folder.appendChild(hdr);
    addSection.appendChild(folder);
    list.appendChild(addSection);
  }

  // ── Profile toggles ──────────────────────────────────────────────────────────
  const otherProfileCount = allMatched.filter(s => s.profile && s.profile !== S.activeProfile).length;
  if (otherProfileCount > 0 && !_showAllProfiles) {
    const pfToggle = document.createElement('div');
    pfToggle.style.cssText = 'font-size:10px;padding:4px 10px;color:var(--muted);cursor:pointer;text-align:center;opacity:.7;';
    pfToggle.textContent = 'Show ' + otherProfileCount + ' from other profiles';
    pfToggle.onclick = () => { _showAllProfiles = true; renderSessionListFromCache(); };
    list.appendChild(pfToggle);
  } else if (_showAllProfiles && otherProfileCount > 0) {
    const pfToggle = document.createElement('div');
    pfToggle.style.cssText = 'font-size:10px;padding:4px 10px;color:var(--muted);cursor:pointer;text-align:center;opacity:.7;';
    pfToggle.textContent = 'Show active profile only';
    pfToggle.onclick = () => { _showAllProfiles = false; renderSessionListFromCache(); };
    list.appendChild(pfToggle);
  }

  // ── Empty state ───────────────────────────────────────────────────────────────
  if (sessions.length === 0 && !_activeProject) {
    const empty = document.createElement('div');
    empty.style.cssText = 'padding:20px 14px;color:var(--muted);font-size:12px;text-align:center;opacity:.7;';
    empty.textContent = 'No sessions yet.';
    list.appendChild(empty);
  }
}

// _syncCtxIndicator is used by sessions.ts; declare it here since it's called from sessions.ts
declare function _syncCtxIndicator(usage: {
  input_tokens?: number; output_tokens?: number; estimated_cost?: number;
  context_length?: number; last_prompt_tokens?: number; threshold_tokens?: number;
}): void;

// ── Export to global scope (boot.ts expects these as globals) ─────────────────
(window as any).ICONS = ICONS;
(window as any).newSession = newSession;
(window as any).loadSession = loadSession;
(window as any).renderSessionList = renderSessionList;
(window as any).deleteSession = deleteSession;
(window as any).startGatewaySSE = startGatewaySSE;
(window as any).stopGatewaySSE = stopGatewaySSE;
(window as any).filterSessions = filterSessions;
(window as any).closeSessionActionMenu = closeSessionActionMenu;

export { ICONS, newSession, loadSession, renderSessionList, deleteSession, startGatewaySSE, stopGatewaySSE, filterSessions, closeSessionActionMenu };
