// PRE Web GUI — Bootstrap, routing, state

(() => {
  // Detect server platform (macOS vs Windows) for platform-aware UI
  window.PRE_PLATFORM = 'macos'; // default, updated on first status fetch
  fetch('/api/status').then(r => r.json()).then(d => {
    if (d.platform) {
      window.PRE_PLATFORM = d.platform;
      // Re-render welcome if visible
      if (window.Tutorial && typeof window.Tutorial._renderWelcome === 'function') {
        window.Tutorial._renderWelcome();
      }
    }
  }).catch(() => {});

  // Init themes
  Themes.init();

  // Connect WebSocket
  WS.connect();

  // Track current session — restore across refreshes, default to most recent
  let currentSession = sessionStorage.getItem('pre-session') || localStorage.getItem('pre-last-session') || null;

  // ── Delegate (frontier AI) state ──
  let currentDelegate = 'local'; // 'local', 'claude', 'codex', 'gemini'
  let delegateAvailability = {}; // populated on connect

  // ── Deep Research mode ──
  let researchMode = false;
  let researchFrontier = false; // false = local multi-pass, or 'claude'/'codex'/'gemini'
  let researchProgressEl = null; // tracks the current progress card in the chat
  function setCurrentSession(id) {
    currentSession = id;
    if (id) {
      sessionStorage.setItem('pre-session', id);
      localStorage.setItem('pre-last-session', id);
    }
    // Clear Argus reactions when switching sessions — they're session-specific
    if (window.Argus) window.Argus.clear();
  }

  // ── Background job tracking (cron/trigger events) ──
  const backgroundJobs = new Map(); // cronSessionId → { tokens, toolCount }

  function handleBackgroundEvent(msg) {
    const jobId = msg.cronJobId || msg.cronSessionId;
    const sessionId = msg.cronSessionId;
    if (!backgroundJobs.has(sessionId)) {
      backgroundJobs.set(sessionId, { tokens: [], toolCount: 0, description: '' });
      // Create a feed entry for this background job
      const desc = msg.cronDescription || jobId || 'Background job';
      backgroundJobs.get(sessionId).description = desc;
      if (window.AgentFeed) window.AgentFeed.add(sessionId, desc, sessionId);
    }
    const job = backgroundJobs.get(sessionId);

    if (msg.type === 'token' && msg.content) {
      job.tokens.push(msg.content);
    } else if (msg.type === 'tool_call') {
      job.toolCount++;
      if (window.AgentFeed) window.AgentFeed.addTool(sessionId, msg.name || 'tool');
    } else if (msg.type === 'done') {
      const result = job.tokens.join('');
      if (window.AgentFeed) {
        window.AgentFeed.setResult(sessionId, result);
        window.AgentFeed.complete(sessionId);
      }
      backgroundJobs.delete(sessionId);
    }
    // agent_status events from cron sub-agents (type is 'agent_status', sub-type in event fields)
    if (msg.type === 'agent_status' && msg.tool && window.AgentFeed) {
      window.AgentFeed.addTool(sessionId, msg.tool);
    }
  }

  // ── WebSocket message handler ──
  WS.on('message', (msg) => {
    // Route background job events (cron/triggers) to Agent Feed instead of current chat
    if (msg.cronSessionId) {
      handleBackgroundEvent(msg);
      // Still allow cron_complete through for the toast notification
      if (msg.type !== 'cron_complete') return;
    }

    // Filter out background agent streaming events (tool_call, token, etc.) —
    // only allow the bg_agent lifecycle events through to the main switch
    if (msg.bgAgentId && msg.type !== 'bg_agent') return;

    switch (msg.type) {
      case 'route':
        // Multi-model router tier notification — inject badge as DOM element
        if (msg.tier) {
          if (!Chat.isStreaming) Chat.startStream();
          const tierLabels = { fast: 'Fast', standard: 'Standard', frontier: 'Frontier' };
          const tierColors = { fast: '#10b981', standard: '#6b7280', frontier: '#8b5cf6' };
          const label = tierLabels[msg.tier] || msg.tier;
          // Escalated runs get a distinct teal color and ↑ glyph so the user
          // can see at a glance that the eGPU answered this turn.
          const color = msg.escalated ? '#14b8a6' : (tierColors[msg.tier] || '#6b7280');
          const modelName = msg.model || '';
          const streamMsg = document.getElementById('streaming-message');
          if (streamMsg) {
            const badge = document.createElement('span');
            badge.style.cssText = `display:inline-block;font-size:0.65rem;padding:2px 8px;border-radius:10px;background:${color}15;color:${color};border:1px solid ${color}30;margin-bottom:6px;font-weight:500`;
            badge.textContent = msg.escalated
              ? 'pre-gpu'
              : (modelName ? `${modelName}` : `${label}`);
            const content = streamMsg.querySelector('.message-content');
            if (content) content.parentNode.insertBefore(badge, content);
          }
        }
        break;

      case 'thinking':
        if (!Chat.isStreaming) Chat.startStream();
        Chat.appendThinking(msg.content);
        break;

      case 'token':
        if (!Chat.isStreaming) Chat.startStream();
        Chat.appendToken(msg.content);
        break;

      case 'tool_calls':
        if (msg.calls) {
          // Agent tools (spawn_agent, spawn_multi) are skipped here because
          // they'll get a proper card from the tool_call event with a tracking ID.
          // Creating one here (without an id) would produce an orphan duplicate.
          const AGENT_NAMES = new Set(['spawn_agent', 'spawn_multi']);
          for (const tc of msg.calls) {
            const name = tc.function?.name || '';
            if (AGENT_NAMES.has(name)) continue;
            Chat.addToolCall({
              name,
              args: tc.function?.arguments,
              status: 'pending',
            });
          }
        }
        break;

      case 'tool_call':
        Chat.addToolCall(msg);
        break;

      case 'tool_result':
        Chat.updateToolCard(msg.id, msg.output, msg.status);
        break;

      case 'done_partial':
        // Tool loop intermediate: end current stream before tool execution
        Chat.endStream(msg.stats);
        break;

      case 'done':
        Chat.endStream(msg.stats, msg.context);
        hideStopButton();
        break;

      case 'agent_status':
        Chat.updateAgentStatus(msg);
        break;

      case 'confirm_request':
        showConfirmDialog(msg.id, msg.tool, msg.args);
        break;

      case 'artifact':
        Chat.addArtifactCard(msg.title, msg.path, msg.artifactType);
        break;

      case 'document':
        Chat.addDocumentCard(msg.title, msg.path, msg.artifactType);
        break;

      case 'image_generated':
        Chat.addImageCard(msg.prompt, msg.path);
        break;

      case 'memory_saved':
        if (msg.memories && msg.memories.length > 0) {
          const names = msg.memories.map(m => m.name).join(', ');
          console.log(`[memory] Auto-saved: ${names}`);
        }
        break;

      case 'session_renamed':
        if (msg.sessionId === currentSession) {
          currentDisplayName = msg.name;
          updateSessionName();
          // Keep resume state name in sync
          try {
            const r = JSON.parse(localStorage.getItem('pre-resume-session') || '{}');
            if (r.id === currentSession) {
              r.name = msg.name;
              localStorage.setItem('pre-resume-session', JSON.stringify(r));
            }
          } catch {}
        }
        loadSessionList();
        break;

      case 'argus_thinking':
        if (window.Argus) window.Argus.showThinking();
        break;
      case 'argus_thinking_done':
        if (window.Argus) window.Argus.hideThinking();
        break;
      case 'argus_reaction':
        if (window.Argus) window.Argus.addReaction(msg);
        break;
      case 'argus_reply':
        if (window.Argus) window.Argus.addReply(msg);
        break;

      case 'cron_complete':
        // A cron job finished running server-side — show toast with link to its session
        showCronToast(msg.description, msg.sessionId, msg.preview);
        loadSessionList(); // refresh sidebar to show new cron session
        // Refresh cron panel if it's currently open so stats are up to date
        if (!document.getElementById('right-panel').classList.contains('hidden')
            && document.getElementById('panel-title').textContent === 'Scheduled Jobs') {
          window.Cron.refresh();
        }
        break;

      // ── Delegate (frontier AI) events ──
      case 'delegate_start':
        Chat.startDelegateStream(msg.target);
        break;

      case 'delegate_token':
        Chat.appendDelegateToken(msg.content);
        break;

      case 'delegate_done':
        Chat.endDelegateStream(msg.target, msg.duration);
        hideStopButton();
        break;

      case 'delegate_error': {
        Chat.endDelegateStream(msg.target);
        const name = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini' }[msg.target] || msg.target;
        Chat.addError(`${name}: ${msg.message}`);
        hideStopButton();
        break;
      }

      // ── Deep Research events ──
      case 'research_status':
        handleResearchStatus(msg);
        break;

      case 'error':
        Chat.endStream();
        Chat.addError(msg.message);
        hideStopButton();
        break;

      case 'session_history':
        Chat.loadHistory(msg.messages);
        setCurrentSession(msg.sessionId || currentSession);
        updateSessionName();
        break;

      // ── Background Agent events ──
      case 'bg_agent':
        window.AgentFeed.handleEvent(msg);
        break;

      // ── Tier pin result ──
      case 'tier_result':
        if (msg.success) {
          Chat.addSystemNote(msg.message);
        } else {
          Chat.addError(msg.message);
        }
        break;
      case 'escalation_status':
        updateEscalationBadge(msg);
        break;
    }
  });

  // ── Escalation: chat-input toggle (sidebar badge intentionally absent) ──
  let escalationAvailable = false;
  window.__escalateArmed = false;

  function updateEscalationBadge(status) {
    const btn = document.getElementById('escalate-btn');
    escalationAvailable = !!status?.available;
    if (!btn) return;
    if (escalationAvailable) {
      btn.disabled = false;
      btn.title = `Escalate next message to ${status.label || 'eGPU'} (click to arm)`;
    } else {
      btn.disabled = true;
      btn.classList.remove('armed');
      btn.title = status?.reason ? `Escalation unavailable: ${status.reason}` : 'Escalation unavailable';
      window.__escalateArmed = false;
    }
  }

  // Fetch initial escalation status on load (drives the chat-input toggle's enabled state)
  fetch('/api/escalation/status').then(r => r.json()).then(updateEscalationBadge).catch(() => {});

  // Wire the escalation toggle button
  document.addEventListener('DOMContentLoaded', () => {
    const btn = document.getElementById('escalate-btn');
    if (!btn) return;
    btn.addEventListener('click', () => {
      if (btn.disabled) return;
      window.__escalateArmed = !window.__escalateArmed;
      btn.classList.toggle('armed', window.__escalateArmed);
    });
  });


  function formatDuration(ms) {
    if (ms < 1000) return '<1s';
    const s = Math.round(ms / 1000);
    if (s < 60) return `${s}s`;
    const m = Math.floor(s / 60);
    const rem = s % 60;
    return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
  }

  function showBgAgentToast(name, sessionId, detail, type) {
    const toast = document.createElement('div');
    toast.className = 'cron-toast';
    const icon = type === 'completed'
      ? '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Z"/></svg>'
      : '<svg width="14" height="14" viewBox="0 0 16 16" fill="var(--danger)"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0L7.1 4.995z"/></svg>';
    const statusLabel = type === 'completed' ? 'Agent Complete' : 'Agent Failed';
    const shortDetail = detail && detail.length > 150 ? detail.slice(0, 150) + '...' : (detail || '');
    toast.innerHTML = `
      <div class="cron-toast-header">
        ${icon}
        <strong>${statusLabel}: ${escapeHtml(name)}</strong>
        <button class="cron-toast-close" onclick="this.closest('.cron-toast').remove()">&times;</button>
      </div>
      <div class="cron-toast-preview">${shortDetail.replace(/</g, '&lt;')}</div>
      ${sessionId ? `<button class="btn btn-sm btn-primary cron-toast-open" data-session="${sessionId}">View Result</button>` : ''}
    `;
    document.body.appendChild(toast);
    if (sessionId) {
      toast.querySelector('.cron-toast-open').addEventListener('click', async () => {
        setCurrentSession(sessionId);
        updateSessionName();
        loadSessionList();
        toast.remove();
        try {
          const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}`);
          const messages = await res.json();
          Chat.loadHistory(messages);
        } catch {}
        WS.send({ type: 'switch_session', sessionId });
      });
    }
    setTimeout(() => { if (toast.parentNode) toast.remove(); }, 30000);
  }

  WS.on('open', async () => {
    // Load delegate availability
    try {
      const dRes = await fetch('/api/delegates');
      delegateAvailability = await dRes.json();
      initDelegateSelector();
      initResearchOptions();
    } catch {}

    // Load session list first so we can pick the most recent if needed
    await loadSessionList();

    // Check URL params — ?session=ID deep-links to a specific session (used by cron notifications)
    const urlParams = new URLSearchParams(window.location.search);
    const urlSession = urlParams.get('session');
    if (urlSession) {
      setCurrentSession(urlSession);
      // Clean URL without reloading
      window.history.replaceState({}, '', window.location.pathname);
    }

    if (!currentSession) {
      // No saved session — default to most recent
      try {
        const res = await fetch('/api/sessions');
        const sessions = await res.json();
        if (sessions.length > 0) {
          setCurrentSession(sessions[0].id); // already sorted by most recent
        }
      } catch {}
    }
    if (!currentSession) setCurrentSession('web:general');
    WS.send({ type: 'switch_session', sessionId: currentSession });
    loadSessionList();
  });

  // ── Chat form ──
  const form = document.getElementById('chat-form');
  const input = document.getElementById('chat-input');
  const fileInput = document.getElementById('file-input');
  const attachBtn = document.getElementById('attach-btn');
  const attachPreview = document.getElementById('attachment-preview');

  // Pending attachments waiting to be sent
  let pendingAttachments = [];

  const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']);
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    sendMessage();
  });

  // Textarea auto-resize and keyboard shortcuts
  input.addEventListener('input', () => {
    input.style.height = 'auto';
    input.style.height = Math.min(input.scrollHeight, 200) + 'px';
  });

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

  // Attach button opens file picker
  attachBtn.addEventListener('click', () => fileInput.click());

  // Handle file selection
  fileInput.addEventListener('change', () => {
    for (const file of fileInput.files) {
      if (file.size > MAX_FILE_SIZE) {
        Chat.addError(`File "${file.name}" is too large (max 10MB)`);
        continue;
      }
      readAttachment(file);
    }
    fileInput.value = ''; // reset so same file can be re-selected
  });

  // Drag-and-drop on the input area
  const inputArea = document.querySelector('.input-area');
  inputArea.addEventListener('dragover', (e) => {
    e.preventDefault();
    inputArea.classList.add('drag-over');
  });
  inputArea.addEventListener('dragleave', () => {
    inputArea.classList.remove('drag-over');
  });
  inputArea.addEventListener('drop', (e) => {
    e.preventDefault();
    inputArea.classList.remove('drag-over');
    for (const file of e.dataTransfer.files) {
      if (file.size > MAX_FILE_SIZE) {
        Chat.addError(`File "${file.name}" is too large (max 10MB)`);
        continue;
      }
      readAttachment(file);
    }
  });

  // Paste images from clipboard
  input.addEventListener('paste', (e) => {
    const items = e.clipboardData?.items;
    if (!items) return;
    for (const item of items) {
      if (item.kind === 'file') {
        e.preventDefault();
        const file = item.getAsFile();
        if (file) readAttachment(file);
      }
    }
  });

  function readAttachment(file) {
    const isImage = IMAGE_TYPES.has(file.type);
    const reader = new FileReader();
    reader.onload = () => {
      const attachment = {
        name: file.name,
        type: file.type,
        size: file.size,
        isImage,
      };
      if (isImage) {
        // Store as base64 data URL for preview and base64 raw for Ollama
        attachment.dataUrl = reader.result;
        attachment.base64 = reader.result.split(',')[1]; // strip data:...;base64, prefix
      } else {
        attachment.text = reader.result;
      }
      pendingAttachments.push(attachment);
      renderAttachmentPreview();
    };
    if (isImage) {
      reader.readAsDataURL(file);
    } else {
      reader.readAsText(file);
    }
  }

  function renderAttachmentPreview() {
    if (pendingAttachments.length === 0) {
      attachPreview.classList.add('hidden');
      attachPreview.innerHTML = '';
      return;
    }
    attachPreview.classList.remove('hidden');
    attachPreview.innerHTML = pendingAttachments.map((att, i) => {
      const sizeStr = att.size < 1024 ? `${att.size}B`
        : att.size < 1048576 ? `${(att.size / 1024).toFixed(0)}KB`
        : `${(att.size / 1048576).toFixed(1)}MB`;
      if (att.isImage) {
        return `<div class="attachment-chip image-chip">
          <img src="${att.dataUrl}" alt="${Markdown.escapeHtml(att.name)}">
          <span class="attachment-chip-name">${Markdown.escapeHtml(att.name)}</span>
          <button type="button" class="attachment-remove" onclick="window._removeAttachment(${i})">&times;</button>
        </div>`;
      }
      return `<div class="attachment-chip file-chip">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
        <span class="attachment-chip-name">${Markdown.escapeHtml(att.name)}</span>
        <span class="attachment-chip-size">${sizeStr}</span>
        <button type="button" class="attachment-remove" onclick="window._removeAttachment(${i})">&times;</button>
      </div>`;
    }).join('');
  }

  window._removeAttachment = (index) => {
    pendingAttachments.splice(index, 1);
    renderAttachmentPreview();
  };

  // Reveal a file in Finder (macOS)
  window._revealInFinder = (filePath) => {
    fetch('/api/artifacts/reveal', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ filePath }),
    }).catch(() => {});
  };

  // Export an artifact to PDF, PNG, or self-contained HTML
  window._exportArtifact = async (btn, artPath, format, title) => {
    const origText = btn.textContent;
    btn.textContent = 'Exporting...';
    btn.disabled = true;
    btn.classList.add('btn-share-loading');

    try {
      const res = await fetch('/api/artifacts/export', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ path: artPath, format, title }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Export failed');

      // Trigger download of the exported file
      const a = document.createElement('a');
      a.href = data.path;
      a.download = data.filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);

      btn.textContent = 'Done!';
      setTimeout(() => { btn.textContent = origText; btn.disabled = false; btn.classList.remove('btn-share-loading'); }, 2000);
    } catch (err) {
      btn.textContent = 'Failed';
      setTimeout(() => { btn.textContent = origText; btn.disabled = false; btn.classList.remove('btn-share-loading'); }, 2000);
      console.error('[export]', err);
    }
  };

  // ── Stop button ──
  const sendBtn = document.getElementById('send-btn');
  const stopBtn = document.getElementById('stop-btn');

  function showStopButton() {
    sendBtn.classList.add('hidden');
    stopBtn.classList.remove('hidden');
  }

  function hideStopButton() {
    stopBtn.classList.add('hidden');
    sendBtn.classList.remove('hidden');
  }

  stopBtn.addEventListener('click', () => {
    WS.send({ type: 'cancel' });
    Chat.cancelStream();
    hideStopButton();
  });

  function sendMessage() {
    const content = input.value.trim();
    if ((!content && pendingAttachments.length === 0) || Chat.isStreaming) return;

    // Intercept /tier command — route to server without adding to chat
    const tierMatch = content.match(/^\/tier\s+(fast|standard|frontier|auto)\s*$/i);
    if (tierMatch) {
      WS.send({ type: 'tier', tier: tierMatch[1].toLowerCase() });
      input.value = '';
      input.style.height = 'auto';
      return;
    }

    // Build attachment summaries for display
    const attachments = pendingAttachments.map(a => ({
      name: a.name, isImage: a.isImage, dataUrl: a.dataUrl || null,
      size: a.size, textPreview: a.text ? a.text.slice(0, 200) : null,
    }));

    // Add user message to UI (with attachment info)
    Chat.addMessage('user', content || '(attached files)', { attachments });

    // Show thinking indicator immediately
    Chat.showThinkingIndicator();

    // Build WS message
    const wsMsg = { type: 'message', content: content || '' };

    // Collect images (base64) for Ollama multimodal
    const images = pendingAttachments.filter(a => a.isImage).map(a => a.base64);
    if (images.length > 0) wsMsg.images = images;

    // Collect text files — prepend to content
    const textFiles = pendingAttachments.filter(a => !a.isImage && a.text);
    if (textFiles.length > 0) {
      const fileBlocks = textFiles.map(f => {
        const ext = f.name.split('.').pop() || '';
        return `<file name="${f.name}">\n\`\`\`${ext}\n${f.text}\n\`\`\`\n</file>`;
      }).join('\n\n');
      wsMsg.content = fileBlocks + (wsMsg.content ? '\n\n' + wsMsg.content : '');
    }

    // Attach research mode flags if active
    if (researchMode) {
      wsMsg.researchMode = true;
      if (researchFrontier) wsMsg.useFrontier = researchFrontier;
    }

    // Escalation toggle: armed by clicking #escalate-btn, applies once.
    // Slash command (/escalate) is handled server-side; no parsing here.
    if (window.__escalateArmed) {
      wsMsg.escalate = true;
      window.__escalateArmed = false;
      const escBtn = document.getElementById('escalate-btn');
      if (escBtn) escBtn.classList.remove('armed');
    }

    // Send to server — delegate or local
    if (currentDelegate !== 'local') {
      WS.send({ type: 'delegate', target: currentDelegate, content: wsMsg.content });
    } else {
      WS.send(wsMsg);
    }

    // Show stop button while generating
    showStopButton();

    // Save resume state — track last session the user actually interacted with
    localStorage.setItem('pre-resume-session', JSON.stringify({
      id: currentSession,
      name: currentDisplayName || null,
      time: new Date().toISOString(),
    }));

    // Clear input and attachments
    input.value = '';
    input.style.height = 'auto';
    pendingAttachments = [];
    renderAttachmentPreview();
  }

  // ── Sidebar ──
  const sidebarToggle = document.getElementById('sidebar-toggle');
  const sidebar = document.getElementById('sidebar');

  sidebarToggle.addEventListener('click', () => {
    sidebar.classList.toggle('open');
  });

  // Close sidebar on mobile when clicking outside
  document.addEventListener('click', (e) => {
    if (window.innerWidth <= 768 &&
        sidebar.classList.contains('open') &&
        !sidebar.contains(e.target) &&
        e.target !== sidebarToggle) {
      sidebar.classList.remove('open');
    }
  });

  // Track which drag is active
  let dragSessionId = null;

  // New session button (ungrouped)
  document.getElementById('new-session-btn').addEventListener('click', async () => {
    try {
      const res = await fetch('/api/sessions/new', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ project: 'web', channel: 'general' }),
      });
      const data = await res.json();
      setCurrentSession(data.id);
      WS.send({ type: 'switch_session', sessionId: currentSession });
      loadSessionList();
    } catch (err) {
      console.error('Failed to create session:', err);
    }
  });

  // New project button
  document.getElementById('new-project-btn').addEventListener('click', () => {
    const list = document.getElementById('session-list');
    // Insert inline input at top of list
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'project-rename-input';
    input.placeholder = 'Project name...';
    list.prepend(input);
    input.focus();

    let saved = false;
    const save = async () => {
      if (saved) return;
      saved = true;
      const name = input.value.trim();
      input.remove();
      if (!name) return;
      try {
        await fetch('/api/projects', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name }),
        });
        loadSessionList();
      } catch {}
    };
    input.addEventListener('keydown', (ev) => {
      if (ev.key === 'Enter') { ev.preventDefault(); save(); }
      if (ev.key === 'Escape') { input.remove(); }
    });
    input.addEventListener('blur', save);
  });

  let currentDisplayName = null;

  // ── Build a session item element ──
  function buildSessionItem(session) {
    const item = document.createElement('div');
    item.className = 'session-item' + (session.id === currentSession ? ' active' : '');
    item.draggable = true;
    item.dataset.sessionId = session.id;

    if (session.id === currentSession) {
      currentDisplayName = session.displayName || null;
      updateSessionName();
    }

    const displayLabel = session.displayName || (session.turnCount === 0 ? 'New Session' : session.channel || 'general');

    const info = document.createElement('button');
    info.className = 'session-item-info';
    info.innerHTML = `
      <div class="session-item-name">${escapeHtml(displayLabel)}</div>
      <div class="session-item-preview">${escapeHtml(session.preview || 'New session')}</div>
      <div class="session-item-time">${formatTime(session.modified)}</div>
    `;
    info.addEventListener('click', () => {
      if (session.id === currentSession) return;
      setCurrentSession(session.id);
      WS.send({ type: 'switch_session', sessionId: currentSession });
      loadSessionList();
      if (window.innerWidth <= 768) sidebar.classList.remove('open');
    });
    info.addEventListener('dblclick', (e) => {
      e.preventDefault();
      e.stopPropagation();
      startInlineRename(info, session);
    });

    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'session-delete-btn';
    deleteBtn.title = 'Delete session';
    deleteBtn.innerHTML = '&times;';
    deleteBtn.addEventListener('click', async (e) => {
      e.stopPropagation();
      if (!confirm(`Delete session "${displayLabel}"?`)) return;
      try {
        await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, { method: 'DELETE' });
        if (session.id === currentSession) {
          // Show welcome screen with a fresh session
          showWelcomeScreen();
        }
        loadSessionList();
      } catch {}
    });

    // Drag events
    item.addEventListener('dragstart', (e) => {
      dragSessionId = session.id;
      item.classList.add('dragging');
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', session.id);
      // Show ungrouped drop zone
      const dropZone = document.querySelector('.ungrouped-drop');
      if (dropZone) dropZone.classList.add('visible');
    });
    item.addEventListener('dragend', () => {
      dragSessionId = null;
      item.classList.remove('dragging');
      const dropZone = document.querySelector('.ungrouped-drop');
      if (dropZone) dropZone.classList.remove('visible');
      document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
    });

    item.appendChild(info);
    item.appendChild(deleteBtn);
    return item;
  }

  // ── Collapsed state persistence ──
  function getExpandedProjects() {
    try { return JSON.parse(sessionStorage.getItem('pre-expanded-projects') || '{}'); } catch { return {}; }
  }
  function setProjectExpanded(slug, expanded) {
    const state = getExpandedProjects();
    state[slug] = expanded;
    sessionStorage.setItem('pre-expanded-projects', JSON.stringify(state));
  }

  // ── Session search ──
  let allSessions = [];
  let allProjects = [];
  const searchInput = document.getElementById('session-search');

  searchInput.addEventListener('input', () => {
    renderSessionList(searchInput.value.trim());
  });

  // Escape clears the search
  searchInput.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      searchInput.value = '';
      renderSessionList('');
      searchInput.blur();
    }
  });

  // ── Cron toast notification ──
  // ── Delegate selector UI ──
  const delegateBtn = document.getElementById('delegate-btn');
  const delegateDropdown = document.getElementById('delegate-dropdown');
  const delegateLabel = delegateBtn.querySelector('.delegate-label');

  const DELEGATE_COLORS = { local: null, claude: '#cc785c', codex: '#10a37f', gemini: '#4285f4' };
  const DELEGATE_NAMES = { local: 'PRE', claude: 'Claude', codex: 'Codex', gemini: 'Gemini' };

  function initDelegateSelector() {
    // Hide unavailable delegates, show available ones with version
    for (const [key, info] of Object.entries(delegateAvailability)) {
      const option = delegateDropdown.querySelector(`.delegate-option[data-target="${key}"]`);
      if (!option) continue;
      if (info.available) {
        option.style.display = '';
        const badge = option.querySelector('.delegate-status');
        if (badge) {
          badge.textContent = info.version || 'installed';
          badge.classList.remove('unavailable');
        }
      } else {
        option.style.display = 'none';
      }
    }
    // If no delegates available at all, hide the entire delegate button
    const anyAvailable = Object.values(delegateAvailability).some(d => d.available);
    delegateBtn.style.display = anyAvailable ? '' : 'none';
  }

  delegateBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    delegateDropdown.classList.toggle('hidden');
  });

  // Close dropdown on outside click
  document.addEventListener('click', () => delegateDropdown.classList.add('hidden'));

  // Handle delegate selection
  delegateDropdown.addEventListener('click', (e) => {
    const option = e.target.closest('.delegate-option');
    if (!option) return;
    const target = option.dataset.target;

    // Check availability for non-local delegates
    if (target !== 'local' && delegateAvailability[target] && !delegateAvailability[target].available) {
      const hint = delegateAvailability[target].installHint || '';
      Chat.addError(`${DELEGATE_NAMES[target]} CLI is not installed.${hint ? ' Install with: ' + hint : ''}`);
      return;
    }

    currentDelegate = target;
    delegateDropdown.classList.add('hidden');

    // Update button appearance
    delegateLabel.textContent = DELEGATE_NAMES[target] || 'PRE';
    if (target === 'local') {
      delegateBtn.classList.remove('delegate-active');
      delegateBtn.style.removeProperty('--delegate-color');
      input.placeholder = 'Message PRE...';
    } else {
      delegateBtn.classList.add('delegate-active');
      delegateBtn.style.setProperty('--delegate-color', DELEGATE_COLORS[target]);
      input.placeholder = `Message ${DELEGATE_NAMES[target]}...`;
    }

    // Update active state in dropdown
    delegateDropdown.querySelectorAll('.delegate-option').forEach(o => {
      o.classList.toggle('active', o.dataset.target === target);
    });
  });

  // ── Research mode toggle + dropdown ──
  const researchBtn = document.getElementById('research-btn');
  const researchDropdown = document.getElementById('research-dropdown');

  function updateResearchPlaceholder() {
    if (researchMode) {
      const label = researchFrontier
        ? `Deep Research (${researchFrontier}): ask a research question...`
        : 'Deep Research (local multi-pass): ask a research question...';
      input.placeholder = label;
    } else {
      if (currentDelegate !== 'local') {
        input.placeholder = `Message ${DELEGATE_NAMES[currentDelegate]}...`;
      } else {
        input.placeholder = 'Message PRE...';
      }
    }
  }

  // Click the flask: toggle research on/off
  researchBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (researchMode) {
      // Turn off — hide dropdown too
      researchMode = false;
      researchBtn.classList.remove('research-active');
      researchDropdown.classList.add('hidden');
      updateResearchPlaceholder();
    } else {
      // Turn on — show dropdown for settings
      researchMode = true;
      researchBtn.classList.add('research-active');
      researchDropdown.classList.remove('hidden');
      updateResearchPlaceholder();
    }
  });

  // Right-click the flask: open settings without toggling
  researchBtn.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    e.stopPropagation();
    if (researchMode) {
      researchDropdown.classList.toggle('hidden');
    }
  });

  // Close dropdown on outside click
  document.addEventListener('click', (e) => {
    if (!researchDropdown.contains(e.target) && e.target !== researchBtn) {
      researchDropdown.classList.add('hidden');
    }
  });

  // Handle radio selection in dropdown
  researchDropdown.addEventListener('change', (e) => {
    if (e.target.name === 'research-synth') {
      const val = e.target.value;
      researchFrontier = val === 'local' ? false : val;
      updateResearchPlaceholder();
    }
  });

  // On WS connect, hide unavailable frontier options
  function initResearchOptions() {
    const frontierModels = ['claude', 'codex', 'gemini'];
    for (const model of frontierModels) {
      const radio = researchDropdown.querySelector(`input[value="${model}"]`);
      if (!radio) continue;
      const label = radio.closest('.research-option');
      if (!label) continue;
      const available = delegateAvailability[model]?.available;
      if (!available) {
        label.style.display = 'none';
        radio.disabled = true;
      } else {
        label.style.display = '';
        radio.disabled = false;
      }
    }
  }

  // ── Research progress handler ──
  function handleResearchStatus(msg) {
    const messagesContainer = document.getElementById('messages');

    switch (msg.phase) {
      case 'starting':
        // Create the research progress card
        researchProgressEl = document.createElement('div');
        researchProgressEl.className = 'research-progress';
        researchProgressEl.innerHTML = `
          <div class="research-progress-header">
            <div class="research-progress-spinner"></div>
            <span>Deep Research</span>
          </div>
          <div class="research-progress-bar"><div class="research-progress-fill" style="width:5%"></div></div>
          <div class="research-progress-status">${msg.message}</div>
          <ul class="research-progress-sections"></ul>
        `;
        messagesContainer.appendChild(researchProgressEl);
        Chat.scrollToBottom();
        break;

      case 'outline_done': {
        if (!researchProgressEl) break;
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = '15%';
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) status.textContent = msg.message;
        // Populate section list
        const list = researchProgressEl.querySelector('.research-progress-sections');
        if (list && msg.sections) {
          list.innerHTML = msg.sections.map(s =>
            `<li><span class="research-section-check">-</span> ${s}</li>`
          ).join('');
        }
        Chat.scrollToBottom();
        break;
      }

      case 'gather_section': {
        if (!researchProgressEl) break;
        const total = msg.total || 1;
        const pct = 15 + Math.round((msg.current - 1) / total * 60);
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = `${pct}%`;
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) status.textContent = msg.message;
        // Mark current section as active
        const items = researchProgressEl.querySelectorAll('.research-progress-sections li');
        items.forEach((li, i) => {
          li.className = i < msg.current - 1 ? 'done' : i === msg.current - 1 ? 'active' : '';
          const check = li.querySelector('.research-section-check');
          if (check) check.textContent = i < msg.current - 1 ? '\u2713' : i === msg.current - 1 ? '\u25B6' : '-';
        });
        Chat.scrollToBottom();
        break;
      }

      case 'gather_section_done': {
        if (!researchProgressEl) break;
        const total = msg.total || 1;
        const pct = 15 + Math.round(msg.current / total * 60);
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = `${pct}%`;
        // Mark section done
        const items = researchProgressEl.querySelectorAll('.research-progress-sections li');
        if (items[msg.current - 1]) {
          items[msg.current - 1].className = 'done';
          const check = items[msg.current - 1].querySelector('.research-section-check');
          if (check) check.textContent = '\u2713';
        }
        break;
      }

      case 'synthesize': {
        if (!researchProgressEl) break;
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = '80%';
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) status.textContent = msg.message;
        // Mark all sections done
        researchProgressEl.querySelectorAll('.research-progress-sections li').forEach(li => {
          li.className = 'done';
          const check = li.querySelector('.research-section-check');
          if (check) check.textContent = '\u2713';
        });
        Chat.scrollToBottom();
        break;
      }

      case 'synthesize_progress': {
        if (!researchProgressEl) break;
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) status.textContent = msg.message;
        break;
      }

      case 'assemble': {
        if (!researchProgressEl) break;
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = '95%';
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) status.textContent = msg.message;
        Chat.scrollToBottom();
        break;
      }

      case 'complete': {
        if (!researchProgressEl) break;
        const fill = researchProgressEl.querySelector('.research-progress-fill');
        if (fill) fill.style.width = '100%';
        const spinner = researchProgressEl.querySelector('.research-progress-spinner');
        if (spinner) spinner.style.display = 'none';
        const status = researchProgressEl.querySelector('.research-progress-status');
        if (status) {
          status.textContent = msg.message;
          status.style.color = '#8b5cf6';
        }
        researchProgressEl = null;
        Chat.scrollToBottom();
        break;
      }

      default: {
        // Generic status update
        if (researchProgressEl) {
          const status = researchProgressEl.querySelector('.research-progress-status');
          if (status) status.textContent = msg.message || '';
        }
      }
    }
  }

  function showCronToast(description, sessionId, preview) {
    const toast = document.createElement('div');
    toast.className = 'cron-toast';
    const shortPreview = preview && preview.length > 150 ? preview.slice(0, 150) + '...' : (preview || '');
    toast.innerHTML = `
      <div class="cron-toast-header">
        <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3.5a.5.5 0 0 0-1 0V8a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 7.71V3.5z"/><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/></svg>
        <strong>Cron: ${description}</strong>
        <button class="cron-toast-close" onclick="this.closest('.cron-toast').remove()">&times;</button>
      </div>
      <div class="cron-toast-preview">${shortPreview.replace(/</g, '&lt;')}</div>
      <button class="btn btn-sm btn-primary cron-toast-open" data-session="${sessionId}">View Result</button>
    `;
    document.body.appendChild(toast);
    // Click to switch to the cron session
    toast.querySelector('.cron-toast-open').addEventListener('click', async () => {
      setCurrentSession(sessionId);
      updateSessionName();
      loadSessionList();
      toast.remove();
      // Load via REST (reliable even when WS is disconnected)
      try {
        const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}`);
        const messages = await res.json();
        Chat.loadHistory(messages);
      } catch {}
      WS.send({ type: 'switch_session', sessionId });
    });
    // Auto-dismiss after 30 seconds
    setTimeout(() => { if (toast.parentNode) toast.remove(); }, 30000);
  }

  async function loadSessionList() {
    try {
      const [sessionsRes, projectsRes] = await Promise.all([
        fetch('/api/sessions'),
        fetch('/api/projects'),
      ]);
      allSessions = await sessionsRes.json();
      allProjects = await projectsRes.json();
      renderSessionList(searchInput.value.trim());
    } catch {}
  }

  function renderSessionList(searchQuery) {
    const query = (searchQuery || '').toLowerCase();

    // Filter out agent sub-sessions from the sidebar (still accessible via search or Agent Feed)
    let sessions = allSessions.filter(s => {
      const name = (s.displayName || s.channel || '').toLowerCase();
      const id = (s.id || '').toLowerCase();
      // Hide sessions that look like agent sub-sessions
      if (name.startsWith('agent-') || id.includes(':agent-') || id.includes(':agent_')) return false;
      return true;
    });

    // Apply search query (search restores hidden agent sessions so they're still findable)
    if (query) {
      sessions = allSessions.filter(s => {
        const name = (s.displayName || s.channel || '').toLowerCase();
        const preview = (s.preview || '').toLowerCase();
        const id = (s.id || '').toLowerCase();
        return name.includes(query) || preview.includes(query) || id.includes(query);
      });
    }

    const projects = allProjects;
    const list = document.getElementById('session-list');
    list.innerHTML = '';

    const expandedState = getExpandedProjects();

    // Group sessions by projectSlug
    const grouped = {};
    const ungrouped = [];
    for (const s of sessions) {
      if (s.projectSlug) {
        if (!grouped[s.projectSlug]) grouped[s.projectSlug] = [];
        grouped[s.projectSlug].push(s);
      } else {
        ungrouped.push(s);
      }
    }

      // Render ungrouped drop zone (hidden until drag starts)
      const ungroupedDrop = document.createElement('div');
      ungroupedDrop.className = 'ungrouped-drop';
      ungroupedDrop.textContent = 'Drop here to ungroup';
      ungroupedDrop.addEventListener('dragover', (e) => { e.preventDefault(); ungroupedDrop.classList.add('drag-over'); });
      ungroupedDrop.addEventListener('dragleave', () => { ungroupedDrop.classList.remove('drag-over'); });
      ungroupedDrop.addEventListener('drop', async (e) => {
        e.preventDefault();
        ungroupedDrop.classList.remove('drag-over');
        ungroupedDrop.classList.remove('visible');
        const sid = e.dataTransfer.getData('text/plain');
        if (sid) {
          await fetch(`/api/sessions/${encodeURIComponent(sid)}/move`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ projectSlug: null }),
          });
          loadSessionList();
        }
      });
      list.appendChild(ungroupedDrop);

      // Render projects
      for (const proj of projects) {
        const group = document.createElement('div');
        group.className = 'project-group';
        const isExpanded = expandedState[proj.slug] === true; // default collapsed
        if (isExpanded) group.classList.add('expanded');

        const header = document.createElement('div');
        header.className = 'project-header';
        header.innerHTML = `
          <svg class="project-chevron" viewBox="0 0 16 16" fill="currentColor"><path d="M6.5 3l5 5-5 5V3z"/></svg>
          <svg class="project-icon" viewBox="0 0 16 16" fill="currentColor"><path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h3.879a1.5 1.5 0 0 1 1.06.44l1.122 1.12A1.5 1.5 0 0 0 9.62 4H13.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9z"/></svg>
          <span class="project-name">${escapeHtml(proj.name)}</span>
          <span class="project-count">${proj.sessionCount}</span>
          <div class="project-actions">
            <button class="project-add-btn" title="New session in project">+</button>
            <button class="project-delete-btn" title="Delete project">&times;</button>
          </div>
        `;

        // Toggle collapse
        header.addEventListener('click', (e) => {
          if (e.target.closest('.project-actions')) return;
          group.classList.toggle('expanded');
          setProjectExpanded(proj.slug, group.classList.contains('expanded'));
        });

        // Double-click to rename project
        header.addEventListener('dblclick', (e) => {
          e.preventDefault();
          e.stopPropagation();
          const nameEl = header.querySelector('.project-name');
          if (!nameEl) return;
          const currentName = nameEl.textContent;
          const input = document.createElement('input');
          input.type = 'text';
          input.className = 'project-rename-input';
          input.value = currentName;
          nameEl.replaceWith(input);
          input.focus();
          input.select();
          let saved = false;
          const doSave = async () => {
            if (saved) return;
            saved = true;
            const newName = input.value.trim();
            if (newName && newName !== currentName) {
              await fetch(`/api/projects/${encodeURIComponent(proj.slug)}/rename`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ name: newName }),
              });
            }
            loadSessionList();
          };
          input.addEventListener('keydown', (ev) => {
            if (ev.key === 'Enter') { ev.preventDefault(); doSave(); }
            if (ev.key === 'Escape') { loadSessionList(); }
          });
          input.addEventListener('blur', doSave);
        });

        // Add session to project
        header.querySelector('.project-add-btn').addEventListener('click', async (e) => {
          e.stopPropagation();
          try {
            const res = await fetch('/api/sessions/new', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ project: 'web', channel: 'general', projectSlug: proj.slug }),
            });
            const data = await res.json();
            setCurrentSession(data.id);
            setProjectExpanded(proj.slug, true);
            WS.send({ type: 'switch_session', sessionId: currentSession });
            loadSessionList();
          } catch {}
        });

        // Delete project
        header.querySelector('.project-delete-btn').addEventListener('click', async (e) => {
          e.stopPropagation();
          if (!confirm(`Delete project "${proj.name}"? Sessions will be ungrouped, not deleted.`)) return;
          await fetch(`/api/projects/${encodeURIComponent(proj.slug)}`, { method: 'DELETE' });
          loadSessionList();
        });

        // Drop target for drag-and-drop
        header.addEventListener('dragover', (e) => { e.preventDefault(); header.classList.add('drag-over'); });
        header.addEventListener('dragleave', () => { header.classList.remove('drag-over'); });
        header.addEventListener('drop', async (e) => {
          e.preventDefault();
          header.classList.remove('drag-over');
          const sid = e.dataTransfer.getData('text/plain');
          if (sid) {
            await fetch(`/api/sessions/${encodeURIComponent(sid)}/move`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ projectSlug: proj.slug }),
            });
            setProjectExpanded(proj.slug, true);
            loadSessionList();
          }
        });

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

        const projectSessions = grouped[proj.slug] || [];

        // During search, skip empty projects and force-expand those with matches
        if (query && projectSessions.length === 0) continue;
        if (query && projectSessions.length > 0) {
          group.classList.add('expanded');
        }

        for (const session of projectSessions) {
          sessionsContainer.appendChild(buildSessionItem(session));
        }

        group.appendChild(header);
        group.appendChild(sessionsContainer);
        list.appendChild(group);
      }

      // Render ungrouped sessions
      if (ungrouped.length > 0 && projects.length > 0) {
        const label = document.createElement('div');
        label.className = 'sidebar-label';
        label.style.marginTop = '8px';
        label.textContent = query ? 'Matches' : 'Recent';
        list.appendChild(label);
      }
      const maxShown = query ? 50 : 20;
      for (const session of ungrouped.slice(0, maxShown)) {
        list.appendChild(buildSessionItem(session));
      }

      // No results message
      if (query && sessions.length === 0) {
        const noResults = document.createElement('div');
        noResults.style.cssText = 'color:var(--text-muted);font-size:0.82rem;padding:16px 0;text-align:center';
        noResults.textContent = `No sessions matching "${searchQuery}"`;
        list.appendChild(noResults);
      }
  }

  // Inline rename helper for sidebar items
  function startInlineRename(infoEl, session) {
    const nameEl = infoEl.querySelector('.session-item-name');
    if (!nameEl) return;
    const currentName = nameEl.textContent;
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'session-rename-input';
    input.value = currentName;
    nameEl.replaceWith(input);
    input.focus();
    input.select();

    let saved = false;
    const save = async () => {
      if (saved) return;
      saved = true;
      const newName = input.value.trim();
      try {
        await fetch(`/api/sessions/${encodeURIComponent(session.id)}/rename`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name: newName || null }),
        });
      } catch {}
      loadSessionList();
    };
    input.addEventListener('keydown', (ev) => {
      if (ev.key === 'Enter') { ev.preventDefault(); save(); }
      if (ev.key === 'Escape') { loadSessionList(); }
    });
    input.addEventListener('blur', save);
  }

  // Topbar session name — click to rename
  function setupTopbarRename() {
    const el = document.getElementById('session-name');
    if (!el) return;
    el.style.cursor = 'pointer';
    el.title = 'Click to rename session';
    el.addEventListener('click', () => {
      const currentName = el.textContent;
      const input = document.createElement('input');
      input.type = 'text';
      input.className = 'session-rename-input topbar-rename';
      input.value = currentName;
      el.replaceWith(input);
      input.focus();
      input.select();

      let saved = false;
      const save = async () => {
        if (saved) return;
        saved = true;
        const newName = input.value.trim();
        try {
          await fetch(`/api/sessions/${encodeURIComponent(currentSession)}/rename`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: newName || null }),
          });
        } catch {}
        // Restore the span
        const newEl = document.createElement('span');
        newEl.id = 'session-name';
        input.replaceWith(newEl);
        loadSessionList();
        setupTopbarRename();
      };
      input.addEventListener('keydown', (ev) => {
        if (ev.key === 'Enter') { ev.preventDefault(); save(); }
        if (ev.key === 'Escape') {
          const newEl = document.createElement('span');
          newEl.id = 'session-name';
          newEl.textContent = currentName;
          input.replaceWith(newEl);
          setupTopbarRename();
        }
      });
      input.addEventListener('blur', save);
    });
  }
  setupTopbarRename();

  function updateSessionName() {
    const el = document.getElementById('session-name');
    if (el) {
      if (currentDisplayName) {
        el.textContent = currentDisplayName;
      } else {
        el.textContent = 'New Session';
      }
    }
  }

  // Show the welcome screen by creating a fresh empty session
  async function showWelcomeScreen() {
    try {
      const res = await fetch('/api/sessions/new', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ project: 'web', channel: 'general' }),
      });
      const data = await res.json();
      setCurrentSession(data.id);
      currentDisplayName = null;
      updateSessionName();
      WS.send({ type: 'switch_session', sessionId: currentSession });
      loadSessionList();
    } catch (err) {
      console.error('Failed to show welcome screen:', err);
    }
  }

  // Logo click → welcome screen
  document.querySelector('.logo').addEventListener('click', () => {
    showWelcomeScreen();
  });

  function formatTime(isoString) {
    if (!isoString) return '';
    const d = new Date(isoString);
    const now = new Date();
    const diff = now - d;
    if (diff < 60000) return 'just now';
    if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
    if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
    return d.toLocaleDateString();
  }

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

  // ── Tool confirmation dialog ──
  function showConfirmDialog(id, toolName, args) {
    // Create overlay
    const overlay = document.createElement('div');
    overlay.className = 'confirm-overlay';
    overlay.innerHTML = `
      <div class="confirm-dialog">
        <div class="confirm-title">Confirm Tool Execution</div>
        <div class="confirm-body">
          <div class="confirm-tool">${escapeHtml(toolName)}</div>
          <pre class="confirm-args"><code>${escapeHtml(JSON.stringify(args, null, 2))}</code></pre>
        </div>
        <div class="confirm-actions">
          <button class="btn btn-ghost" id="confirm-deny">Deny</button>
          <button class="btn btn-primary" id="confirm-approve">Approve</button>
        </div>
      </div>
    `;
    document.body.appendChild(overlay);

    overlay.querySelector('#confirm-approve').addEventListener('click', () => {
      WS.send({ type: 'confirm', id, approved: true });
      overlay.remove();
    });
    overlay.querySelector('#confirm-deny').addEventListener('click', () => {
      WS.send({ type: 'confirm', id, approved: false });
      overlay.remove();
    });
  }

  // ── Right panel close ──
  const panelClose = document.getElementById('panel-close');
  if (panelClose) {
    panelClose.addEventListener('click', () => {
      document.getElementById('right-panel').classList.add('hidden');
    });
  }


  // ── Settings panel ── (extracted to js/panels/settings.js)
  // ── Cron jobs panel ── (extracted to js/panels/cron.js)

  // Register navigateSession event for panel modules that need to switch sessions
  PRE.on('navigateSession', async (sessionId) => {
    setCurrentSession(sessionId);
    updateSessionName();
    loadSessionList();
    try {
      const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}`);
      const messages = await res.json();
      Chat.loadHistory(messages);
    } catch {}
    WS.send({ type: 'switch_session', sessionId });
  });

  // Register bgAgent events for toast notifications (agent-feed panel emits these)
  PRE.on('bgAgentCompleted', ({ name, sessionId, result }) => {
    showBgAgentToast(name, sessionId, result, 'completed');
    loadSessionList();
  });
  PRE.on('bgAgentFailed', ({ name, sessionId, error }) => {
    showBgAgentToast(name, sessionId, error, 'failed');
  });

  // ── Memory browser panel ── (extracted to js/panels/memory.js)
  // ── Triggers panel ── (extracted to js/panels/triggers.js)
  // ── RAG panel ── (extracted to js/panels/rag.js)
  // ── Workflow panel ── (extracted to js/panels/workflows.js)
  // ── Agent Feed panel ── (extracted to js/panels/agent-feed.js)
  // ── Dashboard, Tutorial & Onboarding ── (extracted to js/panels/dashboard.js)
  // ── Voice input ── (extracted to js/panels/voice.js)
  // ── Argus companion widget ── (extracted to js/panels/argus.js)
  // ── Quick Actions bar ── (extracted to js/panels/quick-actions.js)
})();
