"""
Live View HTML template served in a WKWebView pane.
Connects to the SSE endpoint and renders a visual timeline, progress bar, preview area, and lightbox.
"""

LIVE_VIEW_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live View</title>
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #FFFFFF; --bg-secondary: #F8F9FA; --text: #1A1A1A;
    --text-secondary: #6B7280; --border: #E5E7EB; --accent: #207CE4;
    --accent-bg: rgba(32,124,228,0.08); --success: #22C55E;
    --error: #EF4444; --error-bg: #FEF2F2;
    --progress-bg: #E5E7EB; --progress-fill: #207CE4;
    --card-bg: #FFFFFF; --card-shadow: 0 1px 3px rgba(0,0,0,0.06);
    --preview-bg: #F3F4F6; --badge-bg: #E0E7FF; --badge-text: #3730A3;
    --radius: 12px; --radius-sm: 8px;
    --thumb-size: 52px;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #1C1C1E; --bg-secondary: #2C2C2E; --text: #E5E5EA;
      --text-secondary: #8E8E93; --border: #3A3A3C; --accent: #4DA3FF;
      --accent-bg: rgba(77,163,255,0.12);
      --error-bg: #3C1414;
      --progress-bg: #3A3A3C; --progress-fill: #4DA3FF;
      --card-bg: #2C2C2E; --card-shadow: 0 1px 3px rgba(0,0,0,0.3);
      --preview-bg: #1C1C1E; --badge-bg: #312E81; --badge-text: #A5B4FC;
    }
  }
  html { color-scheme: light dark; }
  html, body {
    height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
    font-size: 14px; background: var(--bg); color: var(--text); overflow: hidden;
  }
  body { display: flex; flex-direction: column; }

  /* Scrollbar theming */
  ::-webkit-scrollbar { width: 8px; height: 8px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb {
    background: var(--text-secondary); border-radius: 4px;
    border: 2px solid transparent; background-clip: padding-box;
  }
  ::-webkit-scrollbar-thumb:hover { background: var(--text); background-clip: padding-box; }

  /* Header */
  .header {
    flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
    padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--bg);
    -webkit-app-region: drag;
  }
  .header h1 { font-size: 15px; font-weight: 600; }
  .header .status-dot {
    width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary);
    display: inline-block; margin-right: 8px; flex-shrink: 0;
  }
  .header .status-dot.connected { background: var(--success); }
  .header .status-dot.error { background: var(--error); }

  /* Progress section */
  .progress-section {
    flex-shrink: 0; padding: 12px 16px; border-bottom: 1px solid var(--border);
    background: var(--bg);
  }
  .progress-label {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 6px; font-size: 13px; color: var(--text-secondary);
  }
  .progress-bar-track {
    width: 100%; height: 6px; background: var(--progress-bg); border-radius: 3px;
    overflow: hidden;
  }
  .progress-bar-fill {
    height: 100%; background: var(--progress-fill); border-radius: 3px;
    transition: width 0.4s ease; width: 0%;
  }
  .progress-bar-track.indeterminate .progress-bar-fill {
    width: 40% !important;
    animation: progress-indeterminate 1.5s ease-in-out infinite;
  }
  @keyframes progress-indeterminate {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(350%); }
  }

  /* Timeline */
  .timeline-container { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 12px 8px 12px; }
  .timeline-empty {
    display: flex; align-items: center; justify-content: center; height: 100%;
    color: var(--text-secondary); font-size: 13px;
  }

  /* Step rows */
  .step {
    display: flex; gap: 10px; padding: 8px 0; position: relative;
  }
  .step + .step { border-top: 1px solid var(--border); }
  .step-time {
    flex-shrink: 0; width: 50px; font-size: 11px; color: var(--text-secondary);
    padding-top: 2px; font-variant-numeric: tabular-nums;
  }
  .step-thumb {
    flex-shrink: 0; width: var(--thumb-size); height: var(--thumb-size);
    border-radius: 6px; object-fit: cover; cursor: pointer;
    border: 1px solid var(--border); background: var(--bg-secondary);
    transition: transform 0.12s ease;
  }
  .step-thumb:hover { transform: scale(1.06); }
  .step-content { flex: 1; min-width: 0; }
  .step-message { font-size: 13px; line-height: 1.45; word-break: break-word; }
  .step-badge {
    display: inline-block; font-size: 10px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; padding: 2px 6px; border-radius: 4px; margin-right: 6px;
    vertical-align: middle;
  }
  .step-badge.status { background: var(--accent-bg); color: var(--accent); }
  .step-badge.progress { background: var(--badge-bg); color: var(--badge-text); }
  .step-badge.preview { background: rgba(34,197,94,0.1); color: var(--success); }
  .step-badge.error { background: var(--error-bg); color: var(--error); }
  .step-badge.done { background: rgba(34,197,94,0.15); color: var(--success); }
  .step-tool-name {
    font-size: 10px; color: var(--text-secondary); margin-top: 2px;
    font-family: 'SF Mono', Menlo, Consolas, monospace;
  }
  .step-preview-text {
    font-size: 12px; color: var(--text-secondary); margin-top: 4px;
    line-height: 1.4; overflow: hidden; display: -webkit-box;
    -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  }
  .step-url {
    font-size: 11px; color: var(--accent); margin-top: 2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    text-decoration: underline; text-decoration-style: dotted;
    text-underline-offset: 2px;
  }
  .step.has-url:hover { background: var(--accent-bg); }

  /* Card style for preview steps */
  .step.has-preview {
    background: var(--card-bg); border-radius: var(--radius-sm);
    padding: 10px 12px; margin: 4px 0;
    box-shadow: var(--card-shadow); border: 1px solid var(--border);
  }
  .step.has-preview + .step { border-top: none; }
  .step + .step.has-preview { border-top: none; }
  .step-card-image {
    width: 100%; max-height: 200px; object-fit: contain;
    border-radius: 6px; margin-top: 8px; cursor: pointer;
    background: var(--bg-secondary);
  }

  /* Main preview area */
  .preview-section {
    flex-shrink: 0; max-height: 50%; border-top: 1px solid var(--border);
    overflow-y: auto; background: var(--preview-bg); display: none;
  }
  .preview-section.visible { display: block; }
  .preview-inner { padding: 12px 16px; }
  .preview-label {
    font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--text-secondary); margin-bottom: 8px;
  }
  .preview-image {
    max-width: 100%; max-height: 280px; border-radius: var(--radius-sm);
    margin-bottom: 8px; cursor: pointer; object-fit: contain;
  }
  .preview-text {
    font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 12px;
    line-height: 1.5; white-space: pre-wrap; word-break: break-word;
    background: var(--card-bg); padding: 10px 12px; border-radius: var(--radius-sm);
    border: 1px solid var(--border); max-height: 200px; overflow-y: auto;
  }

  /* Lightbox overlay */
  .lightbox {
    display: none; position: fixed; inset: 0; z-index: 999;
    background: rgba(0,0,0,0.82); align-items: center; justify-content: center;
    cursor: zoom-out; -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  }
  .lightbox.visible { display: flex; }
  .lightbox img {
    max-width: 92%; max-height: 92%; object-fit: contain;
    border-radius: var(--radius); box-shadow: 0 8px 40px rgba(0,0,0,0.4);
  }

  /* Cancel / clear buttons */
  .footer {
    flex-shrink: 0; padding: 10px 16px; border-top: 1px solid var(--border);
    background: var(--bg); display: flex; gap: 8px; justify-content: flex-end;
  }
  .btn {
    font-size: 13px; font-weight: 500; padding: 6px 16px; border-radius: 6px;
    border: 1px solid var(--border); background: var(--bg); color: var(--text);
    cursor: pointer; transition: background 0.15s, border-color 0.15s;
    -webkit-app-region: no-drag;
  }
  .btn:hover { background: var(--bg-secondary); }
  .btn:active { opacity: 0.8; }
  .btn.danger { border-color: var(--error); color: var(--error); }
  .btn.danger:hover { background: var(--error-bg); }
  .btn:disabled { opacity: 0.4; cursor: not-allowed; }

  /* Image context menu */
  .img-ctx-menu {
    position: fixed; z-index: 1000; background: var(--card-bg);
    border: 1px solid var(--border); border-radius: 6px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.18); padding: 4px 0;
    min-width: 160px;
  }
  .img-ctx-menu button {
    display: block; width: 100%; text-align: left; padding: 7px 14px;
    font-size: 13px; font-weight: 400; border: none;
    background: transparent; color: var(--text); cursor: pointer;
  }
  .img-ctx-menu button:hover { background: var(--accent-bg); }

  /* Step meta: duration, page, file_path, detail */
  .step-meta {
    font-size: 11px; color: var(--text-secondary); margin-top: 2px;
    font-family: 'SF Mono', Menlo, Consolas, monospace;
  }
  .step-detail, .step-error-detail {
    font-size: 11px; color: var(--text-secondary); margin-top: 4px;
    line-height: 1.4; white-space: pre-wrap; word-break: break-word;
    max-height: 120px; overflow-y: auto;
    background: var(--bg-secondary); padding: 6px 8px; border-radius: 4px;
    border: 1px solid var(--border);
  }
  .step-error-detail { color: var(--error); background: var(--error-bg); border-color: var(--error); }

  /* Run summary card (shown on done) */
  .run-summary {
    flex-shrink: 0; padding: 12px 16px; margin: 8px 12px; border-radius: var(--radius-sm);
    background: var(--accent-bg); border: 1px solid var(--accent);
    font-size: 12px; color: var(--text);
  }
  .run-summary-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; }
  .run-summary-row { margin-top: 4px; color: var(--text-secondary); }
  .run-summary .tools-list { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 11px; }

  /* Header run status (run started / last event ago) */
  .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
  .header #runStatus { font-size: 11px; color: var(--text-secondary); }
</style>
</head>
<body>

<div class="header">
  <div style="display:flex;align-items:center;">
    <span class="status-dot" id="statusDot"></span>
    <h1>Live View</h1>
  </div>
  <div class="header-right">
    <span id="connectionLabel" style="font-size:12px;color:var(--text-secondary);">Idle</span>
    <span id="runStatus"></span>
  </div>
</div>

<div class="progress-section" id="progressSection" style="display:none;">
  <div class="progress-label">
    <span id="progressLabel">Processing…</span>
    <span id="progressPct">0%</span>
  </div>
  <div class="progress-bar-track">
    <div class="progress-bar-fill" id="progressFill"></div>
  </div>
</div>

<div class="timeline-container" id="timelineContainer">
  <div class="timeline-empty" id="emptyState">Waiting for agent activity…</div>
</div>

<div class="preview-section" id="previewSection">
  <div class="preview-inner">
    <div class="preview-label">Live Preview</div>
    <img class="preview-image" id="previewImage" style="display:none;" />
    <div class="preview-text" id="previewText" style="display:none;"></div>
  </div>
</div>

<!-- Lightbox overlay for enlarged images -->
<div class="lightbox" id="lightbox">
  <img id="lightboxImg" src="" alt="Preview" />
</div>

<div class="footer">
  <button class="btn" id="clearBtn" onclick="clearTimeline()">Clear</button>
  <button class="btn danger" id="cancelBtn" onclick="cancelRun()" disabled>Cancel</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<script>
(function() {
  var params = new URLSearchParams(window.location.search);
  var threadId = params.get('thread_id') || '';

  var timelineEl = document.getElementById('timelineContainer');
  var emptyState = document.getElementById('emptyState');
  var progressSection = document.getElementById('progressSection');
  var progressLabel = document.getElementById('progressLabel');
  var progressPct = document.getElementById('progressPct');
  var progressFill = document.getElementById('progressFill');
  var previewSection = document.getElementById('previewSection');
  var previewImage = document.getElementById('previewImage');
  var previewText = document.getElementById('previewText');
  var statusDot = document.getElementById('statusDot');
  var connectionLabel = document.getElementById('connectionLabel');
  var cancelBtn = document.getElementById('cancelBtn');
  var lightbox = document.getElementById('lightbox');
  var lightboxImg = document.getElementById('lightboxImg');

  var eventSource = null;
  var autoScroll = true;
  var heartbeatTimer = null;
  var progressHideTimer = null;
  var reconnectDelay = 1000;
  var MAX_RECONNECT_DELAY = 16000;
  var done = false;
  var stepsForRun = [];
  var firstEventTime = null;
  var lastEventTime = null;
  var stepCount = 0;
  var runStatusInterval = null;

  timelineEl.addEventListener('scroll', function() {
    var gap = timelineEl.scrollHeight - timelineEl.scrollTop - timelineEl.clientHeight;
    autoScroll = gap < 60;
  });

  /* ── Lightbox ──────────────────────────────────────────────────────── */
  function openLightbox(src) {
    lightboxImg.src = src;
    lightbox.classList.add('visible');
  }
  lightbox.addEventListener('click', function() {
    lightbox.classList.remove('visible');
    lightboxImg.src = '';
  });
  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && lightbox.classList.contains('visible')) {
      lightbox.classList.remove('visible');
      lightboxImg.src = '';
    }
  });

  /* ── Image context menu (right-click) ─────────────────────────────── */
  var imgCtxMenu = null;
  function dismissImgCtx() {
    if (imgCtxMenu) { imgCtxMenu.remove(); imgCtxMenu = null; }
  }
  document.addEventListener('contextmenu', function(e) {
    var img = e.target.closest('img.step-thumb, img.step-card-image, img.preview-image, #lightboxImg');
    if (!img) return;
    e.preventDefault();
    dismissImgCtx();
    var src = img.src || '';
    if (!src) return;
    imgCtxMenu = document.createElement('div');
    imgCtxMenu.className = 'img-ctx-menu';
    imgCtxMenu.style.left = e.clientX + 'px';
    imgCtxMenu.style.top = e.clientY + 'px';
    var saveBtn = document.createElement('button');
    saveBtn.textContent = 'Save Image\u2026';
    saveBtn.onclick = function() {
      dismissImgCtx();
      function doSave(dataUrl) {
        try {
          if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.saveImageFromLiveView) {
            window.webkit.messageHandlers.saveImageFromLiveView.postMessage({ dataUrl: dataUrl });
          }
        } catch (ex) {}
      }
      if (src.indexOf('data:') === 0) { doSave(src); }
      else {
        fetch(src).then(function(r) { return r.blob(); }).then(function(blob) {
          var reader = new FileReader();
          reader.onload = function() { doSave(reader.result); };
          reader.readAsDataURL(blob);
        }).catch(function() {});
      }
    };
    imgCtxMenu.appendChild(saveBtn);
    var imgHash = img.getAttribute('data-image-hash');
    if (imgHash) {
      var delBtn = document.createElement('button');
      delBtn.textContent = 'Delete Image';
      delBtn.style.color = 'var(--error)';
      delBtn.onclick = function() {
        dismissImgCtx();
        fetch('/live_view/delete_image', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ hash: imgHash }),
        }).then(function() {
          document.querySelectorAll('img[data-image-hash="' + imgHash + '"]').forEach(function(el) {
            var step = el.closest('.step');
            el.remove();
            if (step) { step.classList.remove('has-preview'); }
          });
          if (previewImage.getAttribute('data-image-hash') === imgHash) {
            previewImage.style.display = 'none';
            previewImage.removeAttribute('data-image-hash');
            if (!previewText.textContent) previewSection.classList.remove('visible');
          }
        }).catch(function() {});
      };
      imgCtxMenu.appendChild(delBtn);
    }
    document.body.appendChild(imgCtxMenu);
  }, true);
  document.addEventListener('click', function() { dismissImgCtx(); }, true);
  document.addEventListener('keydown', function(e) { if (e.key === 'Escape') dismissImgCtx(); });

  /* ── Helpers ───────────────────────────────────────────────────────── */
  function formatTime(isoStr) {
    try {
      var d = new Date(isoStr);
      return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    } catch(e) { return ''; }
  }
  function nowTime() {
    return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
  }

  function formatAgo(isoStr) {
    if (!isoStr) return '';
    try {
      var sec = (Date.now() - new Date(isoStr).getTime()) / 1000;
      if (sec < 60) return 'just now';
      if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
      if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
      return Math.floor(sec / 86400) + 'd ago';
    } catch (e) { return ''; }
  }

  function updateRunStatus() {
    var el = document.getElementById('runStatus');
    if (!el) return;
    if (!firstEventTime && !lastEventTime) {
      el.textContent = '';
      return;
    }
    var parts = [];
    if (stepCount > 0) parts.push('Step ' + stepCount);
    if (firstEventTime) parts.push('Run started ' + formatAgo(firstEventTime));
    if (lastEventTime && lastEventTime !== firstEventTime) parts.push('Last event ' + formatAgo(lastEventTime));
    el.textContent = parts.join(' · ');
  }

  function badgeClass(type) {
    var m = { status: 'status', progress: 'progress', preview: 'preview', error: 'error', done: 'done' };
    return m[type] || 'status';
  }

  function escapeHtml(str) {
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(str));
    return div.innerHTML;
  }

  function imgSrc(b64) {
    if (!b64) return '';
    if (b64.charAt(0) === '/') return 'data:image/jpeg;base64,' + b64;
    if (b64.substring(0, 4) === 'iVBO') return 'data:image/png;base64,' + b64;
    return 'data:image/jpeg;base64,' + b64;
  }

  function eventImgSrc(event) {
    if (event.preview_image_base64) return imgSrc(event.preview_image_base64);
    if (event.preview_image_hash) return '/live_view/image/' + encodeURIComponent(event.preview_image_hash);
    return '';
  }

  function eventHasImage(event) {
    return !!(event.preview_image_base64 || event.preview_image_hash);
  }

  function openExternalUrl(url) {
    if (!url) return;
    fetch('/open?url=' + encodeURIComponent(url)).catch(function(){});
  }

  function _showInViewer(dataUrl) {
    try {
      if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.showImageInWindow) {
        window.webkit.messageHandlers.showImageInWindow.postMessage({ dataUrl: dataUrl });
        return;
      }
    } catch (e) {}
    openLightbox(dataUrl);
  }

  function openImageInViewer(src) {
    if (!src) return;
    if (src.indexOf('data:') === 0) { _showInViewer(src); return; }
    fetch(src).then(function(r) { return r.blob(); }).then(function(blob) {
      var reader = new FileReader();
      reader.onload = function() { _showInViewer(reader.result); };
      reader.readAsDataURL(blob);
    }).catch(function() { openLightbox(src); });
  }

  /* ── Plugin renderer registry ────────────────────────────────────── */
  window._pluginRenderers = {};

  function sanitizePluginHtml(html) {
    if (typeof DOMPurify !== 'undefined') {
      return DOMPurify.sanitize(html, {
        ALLOWED_TAGS: ['div','span','p','br','strong','b','em','i','u','s','a','code','pre','ul','ol','li','h1','h2','h3','h4','h5','h6','table','thead','tbody','tr','th','td','hr','img','sup','sub','details','summary'],
        ALLOWED_ATTR: ['href','src','alt','class','title','target','rel','width','height'],
        ALLOW_DATA_ATTR: false,
        FORBID_ATTR: ['style','onerror','onload','onclick','onmouseover'],
      });
    }
    return escapeHtml(html);
  }

  function showRunSummary() {
    var existing = document.getElementById('runSummaryCard');
    if (existing) existing.remove();
    if (stepsForRun.length === 0) return;
    var steps = stepsForRun;
    var totalSteps = steps.length;
    var toolsSet = {};
    for (var i = 0; i < steps.length; i++) {
      var tn = steps[i].tool_name;
      if (tn) toolsSet[tn] = true;
    }
    var toolsList = Object.keys(toolsSet).sort();
    var firstTs = steps[0].timestamp;
    var lastTs = steps[steps.length - 1].timestamp;
    var durationSec = 0;
    if (firstTs && lastTs) {
      try {
        durationSec = (new Date(lastTs).getTime() - new Date(firstTs).getTime()) / 1000;
      } catch (e) {}
    }
    var durationStr = durationSec < 60 ? durationSec.toFixed(1) + 's' : Math.floor(durationSec / 60) + 'm ' + Math.round(durationSec % 60) + 's';
    var errorCount = 0;
    for (var j = 0; j < steps.length; j++) {
      if (steps[j].type === 'error') errorCount++;
    }
    var card = document.createElement('div');
    card.id = 'runSummaryCard';
    card.className = 'run-summary';
    var title = document.createElement('div');
    title.className = 'run-summary-title';
    title.textContent = 'Run summary';
    card.appendChild(title);
    var row1 = document.createElement('div');
    row1.className = 'run-summary-row';
    row1.textContent = totalSteps + ' step' + (totalSteps !== 1 ? 's' : '') + ' · ' + durationStr + (errorCount > 0 ? ' · ' + errorCount + ' error' + (errorCount !== 1 ? 's' : '') : '');
    card.appendChild(row1);
    if (toolsList.length > 0) {
      var row2 = document.createElement('div');
      row2.className = 'run-summary-row tools-list';
      row2.textContent = 'Tools: ' + toolsList.join(', ');
      card.appendChild(row2);
    }
    timelineEl.insertBefore(card, timelineEl.firstChild);
  }

  /* ── Add timeline step ─────────────────────────────────────────────── */
  function addStep(event) {
    if (emptyState) { emptyState.remove(); emptyState = null; }

    // Check for plugin custom renderer first
    var pluginRenderer = window._pluginRenderers[event.type];
    if (pluginRenderer) {
      try {
        var customHtml = pluginRenderer(event);
        if (customHtml) {
          var wrapper = document.createElement('div');
          wrapper.className = 'step plugin-step';
          var time = document.createElement('div');
          time.className = 'step-time';
          time.textContent = event.timestamp ? formatTime(event.timestamp) : nowTime();
          wrapper.appendChild(time);
          var customContent = document.createElement('div');
          customContent.className = 'step-content plugin-content';
          customContent.innerHTML = sanitizePluginHtml(customHtml);
          wrapper.appendChild(customContent);
          timelineEl.appendChild(wrapper);
          if (autoScroll) timelineEl.scrollTop = timelineEl.scrollHeight;
          return;
        }
      } catch(e) {
        console.warn('Plugin renderer error for ' + event.type + ':', e);
      }
    }

    var hasImage = eventHasImage(event);
    var hasPreviewText = !!event.preview_text;
    var isVisual = hasImage || hasPreviewText;
    var hasUrl = !!event.url;

    var step = document.createElement('div');
    step.className = 'step' + (isVisual ? ' has-preview' : '') + (hasUrl ? ' has-url' : '');
    if (hasUrl) {
      step.style.cursor = 'pointer';
      step.setAttribute('data-url', event.url);
      step.addEventListener('click', function(e) {
        if (e.target.tagName === 'IMG') return;
        openExternalUrl(event.url);
      });
    }

    // Timestamp
    var time = document.createElement('div');
    time.className = 'step-time';
    time.textContent = event.timestamp ? formatTime(event.timestamp) : nowTime();

    // Thumbnail in the row (small, for quick visual reference)
    var thumbEl = null;
    if (hasImage) {
      thumbEl = document.createElement('img');
      thumbEl.className = 'step-thumb';
      thumbEl.src = eventImgSrc(event);
      thumbEl.alt = 'Preview';
      if (event.preview_image_hash) thumbEl.setAttribute('data-image-hash', event.preview_image_hash);
      thumbEl.addEventListener('click', function(e) { e.stopPropagation(); openImageInViewer(this.src); });
      thumbEl.addEventListener('load', function() { if (autoScroll) timelineEl.scrollTop = timelineEl.scrollHeight; });
    }

    // Content column
    var content = document.createElement('div');
    content.className = 'step-content';

    var msg = document.createElement('div');
    msg.className = 'step-message';
    var badge = '<span class="step-badge ' + badgeClass(event.type) + '">' + (event.type || 'status') + '</span>';
    msg.innerHTML = badge + escapeHtml(event.message || '');
    content.appendChild(msg);

    if (event.tool_name) {
      var tn = document.createElement('div');
      tn.className = 'step-tool-name';
      tn.textContent = event.tool_name;
      content.appendChild(tn);
    }

    /* Meta line: duration, page N of M, file_path */
    var metaParts = [];
    if (event.duration_sec != null) metaParts.push('Completed in ' + event.duration_sec + 's');
    if (event.page != null && event.total_pages != null) metaParts.push('Page ' + event.page + ' of ' + event.total_pages);
    else if (event.page != null) metaParts.push('Page ' + event.page);
    if (event.file_path) metaParts.push(event.file_path);
    if (metaParts.length > 0) {
      var meta = document.createElement('div');
      meta.className = 'step-meta';
      meta.textContent = metaParts.join(' · ');
      content.appendChild(meta);
    }

    if (event.detail) {
      var det = document.createElement('div');
      det.className = 'step-detail';
      det.textContent = event.detail;
      content.appendChild(det);
    }
    if (event.error_detail || (event.type === 'error' && event.detail)) {
      var errDet = document.createElement('div');
      errDet.className = 'step-error-detail';
      errDet.textContent = event.error_detail || event.detail;
      content.appendChild(errDet);
    }

    if (hasUrl) {
      var link = document.createElement('div');
      link.className = 'step-url';
      link.textContent = event.url;
      content.appendChild(link);
    }

    // Card image (larger, below message) for preview steps
    if (hasImage) {
      var cardImg = document.createElement('img');
      cardImg.className = 'step-card-image';
      cardImg.src = eventImgSrc(event);
      cardImg.alt = 'Preview';
      if (event.preview_image_hash) cardImg.setAttribute('data-image-hash', event.preview_image_hash);
      cardImg.addEventListener('click', function(e) { e.stopPropagation(); openImageInViewer(this.src); });
      cardImg.addEventListener('load', function() { if (autoScroll) timelineEl.scrollTop = timelineEl.scrollHeight; });
      content.appendChild(cardImg);
    }

    if (hasPreviewText) {
      var pt = document.createElement('div');
      pt.className = 'step-preview-text';
      pt.textContent = event.preview_text;
      content.appendChild(pt);
    }

    step.appendChild(time);
    if (thumbEl) step.appendChild(thumbEl);
    step.appendChild(content);
    timelineEl.appendChild(step);
    if (autoScroll) timelineEl.scrollTop = timelineEl.scrollHeight;
  }

  /* ── Progress ──────────────────────────────────────────────────────── */
  function hideProgressBar() {
    if (progressHideTimer) { clearTimeout(progressHideTimer); progressHideTimer = null; }
    progressSection.style.display = 'none';
    progressFill.parentElement.classList.remove('indeterminate');
    progressFill.style.width = '0%';
    progressPct.textContent = '0%';
  }
  function updateProgress(pct, label, indeterminate) {
    if (pct == null && label == null && !indeterminate) return;
    if (progressHideTimer) { clearTimeout(progressHideTimer); progressHideTimer = null; }
    progressSection.style.display = '';
    var track = progressFill.parentElement;
    if (indeterminate) {
      track.classList.add('indeterminate');
      progressPct.textContent = '';
    } else {
      track.classList.remove('indeterminate');
      if (pct != null) {
        var p = Math.max(0, Math.min(100, pct));
        progressFill.style.width = p + '%';
        progressPct.textContent = Math.round(p) + '%';
      }
    }
    if (label) progressLabel.textContent = label;
  }

  /* ── Main preview (bottom area) ────────────────────────────────────── */
  function updatePreview(event) {
    var hasPreview = false;
    if (eventHasImage(event)) {
      previewImage.src = eventImgSrc(event);
      previewImage.style.display = '';
      if (event.preview_image_hash) previewImage.setAttribute('data-image-hash', event.preview_image_hash);
      else previewImage.removeAttribute('data-image-hash');
      previewImage.style.cursor = 'pointer';
      previewImage.onclick = function() { openImageInViewer(previewImage.src); };
      hasPreview = true;
    }
    if (event.preview_text) {
      previewText.textContent = event.preview_text;
      previewText.style.display = '';
      hasPreview = true;
    }
    if (hasPreview) previewSection.classList.add('visible');
  }

  /* ── Connection state ──────────────────────────────────────────────── */
  function setConnectionState(state) {
    statusDot.className = 'status-dot';
    if (state === 'connected') { statusDot.classList.add('connected'); connectionLabel.textContent = 'Connected'; }
    else if (state === 'error') { statusDot.classList.add('error'); connectionLabel.textContent = 'Disconnected'; }
    else { connectionLabel.textContent = 'Idle'; }
  }

  /* ── Handle incoming event ─────────────────────────────────────────── */
  function handleEvent(event) {
    var type = event.type || 'status';
    addStep(event);
    stepsForRun.push(event);
    if (event.timestamp) {
      lastEventTime = event.timestamp;
      if (!firstEventTime) firstEventTime = event.timestamp;
    }
    stepCount = stepsForRun.length;
    updateRunStatus();
    if (!runStatusInterval) runStatusInterval = setInterval(updateRunStatus, 1000);

    if (type === 'progress' || event.progress_pct != null || event.progress_indeterminate) {
      updateProgress(event.progress_pct, event.message, !!event.progress_indeterminate);
    }
    if (eventHasImage(event) || event.preview_text) {
      updatePreview(event);
    }
    if (type === 'error') { cancelBtn.disabled = true; }
    if (type === 'done') {
      showRunSummary();
      done = true;
      cancelBtn.disabled = true;
      updateProgress(100, 'Complete');
      setConnectionState('connected');
      progressHideTimer = setTimeout(hideProgressBar, 2000);
      /* Reconnect after run ends so Live View receives events on the next round. */
      setTimeout(function() {
        done = false;
        if (eventSource) { try { eventSource.close(); } catch(e){} eventSource = null; }
        connect();
      }, 1500);
    }
  }

  /* ── Clear DOM (internal helper, does NOT touch server history) ───── */
  function clearDom() {
    if (progressHideTimer) { clearTimeout(progressHideTimer); progressHideTimer = null; }
    if (runStatusInterval) { clearInterval(runStatusInterval); runStatusInterval = null; }
    stepsForRun = [];
    firstEventTime = null;
    lastEventTime = null;
    stepCount = 0;
    updateRunStatus();
    var runSummaryCard = document.getElementById('runSummaryCard');
    if (runSummaryCard) runSummaryCard.remove();
    while (timelineEl.firstChild) timelineEl.removeChild(timelineEl.firstChild);
    var empty = document.createElement('div');
    empty.className = 'timeline-empty';
    empty.textContent = 'Waiting for agent activity\u2026';
    emptyState = empty;
    timelineEl.appendChild(empty);
    progressSection.style.display = 'none';
    progressFill.parentElement.classList.remove('indeterminate');
    progressFill.style.width = '0%';
    progressPct.textContent = '0%';
    previewSection.classList.remove('visible');
    previewImage.style.display = 'none';
    previewText.style.display = 'none';
  }

  /* ── Load history from server and replay into timeline ─────────────── */
  function loadHistory(tid, callback) {
    fetch('/live_view/history?thread_id=' + encodeURIComponent(tid))
      .then(function(r) { return r.json(); })
      .then(function(body) {
        var events = Array.isArray(body) ? body : (body.events || []);
        if (body && body.health && body.health.persist_decrypt_failed) {
          var warn = body.warning || 'Live View history could not be restored.';
          handleEvent({type: 'status', message: warn, visible: true});
        }
        if (events && events.length > 0) {
          for (var i = 0; i < events.length; i++) {
            handleEvent(events[i]);
          }
        }
        if (callback) callback();
      })
      .catch(function() {
        if (callback) callback();
      });
  }

  /* ── SSE connection ────────────────────────────────────────────────── */
  function connect() {
    if (!threadId) {
      setConnectionState('idle');
      connectionLabel.textContent = 'No thread_id';
      return;
    }
    if (eventSource) { try { eventSource.close(); } catch(e){} }
    done = false;
    cancelBtn.disabled = false;
    setConnectionState('connected');
    eventSource = new EventSource('/live_view/stream?thread_id=' + encodeURIComponent(threadId));
    eventSource.onopen = function() { reconnectDelay = 1000; setConnectionState('connected'); };
    eventSource.onmessage = function(e) {
      clearTimeout(heartbeatTimer);
      heartbeatTimer = setTimeout(function() { setConnectionState('error'); }, 45000);
      if (!e.data || e.data.trim() === '') return;
      try { handleEvent(JSON.parse(e.data)); } catch(err) {}
    };
    eventSource.onerror = function() {
      setConnectionState('error');
      eventSource.close();
      if (!done) {
        setTimeout(function() { connect(); }, reconnectDelay);
        reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
      }
    };
  }

  /* ── Switch to a different thread (called from native side via JS) ── */
  function switchThread(newThreadId) {
    if (newThreadId === threadId) return;
    if (eventSource) { try { eventSource.close(); } catch(e){} eventSource = null; }
    clearTimeout(heartbeatTimer);
    threadId = newThreadId || '';
    clearDom();
    done = false;
    if (!threadId) {
      setConnectionState('idle');
      connectionLabel.textContent = 'No thread_id';
      return;
    }
    loadHistory(threadId, function() { connect(); });
  }

  function cancelRun() {
    cancelBtn.disabled = true;
    cancelBtn.textContent = 'Stopping\u2026';
    fetch('/trigger_stop', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ thread_id: threadId }),
    }).catch(function(){});
    setTimeout(function() { cancelBtn.textContent = 'Cancel'; }, 3000);
  }

  function clearTimeline() {
    clearDom();
    if (threadId) {
      fetch('/live_view/clear_history', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ thread_id: threadId }),
      }).catch(function(){});
    }
  }

  function reconnectSSE() {
    done = false;
    connect();
  }

  window.liveViewConnect = connect;
  window.liveViewClear = clearTimeline;
  window.cancelRun = cancelRun;
  window.clearTimeline = clearTimeline;
  window.switchThread = switchThread;
  window.reconnectSSE = reconnectSSE;

  if (threadId) {
    loadHistory(threadId, function() { connect(); });
  }
})();
</script>
<!-- PLUGIN_JS_INJECTION_POINT -->
</body>
</html>
"""


def get_live_view_html(plugin_js: str = "") -> str:
    """Return the Live View HTML with optional plugin JS injected."""
    if not plugin_js:
        return LIVE_VIEW_HTML
    injection = f"<script>\n{plugin_js}\n</script>"
    return LIVE_VIEW_HTML.replace("<!-- PLUGIN_JS_INJECTION_POINT -->", injection)
