  var setHtml = function(el, html) { if (el) el.innerHTML = html; };
  var BASE = '';
  var pages = ['overview','sessions','context','memory','skills','agents','scheduler','integrations','metrics','efficiency','recommendations','wallet','settings','workspace'];
  var titles = { overview: 'Overview', sessions: 'Sessions', context: 'Context', memory: 'Memory', skills: 'Skills & Plugins', agents: 'Agents', scheduler: 'Scheduler', integrations: 'Integrations', metrics: 'Observability', efficiency: 'Prompt Performance', wallet: 'Wallet', settings: 'Settings', workspace: 'Workspace' };

  function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
  /** Format memory content: JSON as label:data tables, text with markdown + truncation. */
  function formatMemContent(raw) {
    if (!raw) return '<span style="color:var(--muted)">(empty)</span>';
    var s = String(raw).trim();
    // Try to parse as JSON and render as a readable table
    if ((s.charAt(0) === '{' || s.charAt(0) === '[') && s.length > 2) {
      try {
        var obj = JSON.parse(s);
        return formatMemValue(obj, 0);
      } catch(_) { /* not valid JSON, fall through */ }
    }
    // For non-JSON: convert basic markdown-like formatting and truncate
    return formatMemText(s);
  }
  /** Render a parsed JSON value as label:data rows, recursing into nested objects. */
  function formatMemValue(val, depth) {
    if (val === null || val === undefined) return '<span style="color:var(--muted)">null</span>';
    if (typeof val === 'boolean') return '<span style="color:' + (val ? '#22c55e' : '#ef4444') + '">' + val + '</span>';
    if (typeof val === 'number') return '<span style="color:var(--accent)">' + val + '</span>';
    if (typeof val === 'string') {
      if (val.length === 0) return '<span style="color:var(--muted)">(empty)</span>';
      // If the string itself looks like JSON, try to parse it
      if ((val.charAt(0) === '{' || val.charAt(0) === '[') && val.length > 2) {
        try { return formatMemValue(JSON.parse(val), depth); } catch(_) {}
      }
      // Truncate long strings
      var display = esc(val.length > 300 ? val.substring(0, 300) + '\u2026' : val);
      return '<span style="color:var(--text);word-break:break-word">' + display.replace(/\n/g, '<br>') + '</span>';
    }
    if (Array.isArray(val)) {
      if (val.length === 0) return '<span style="color:var(--muted)">(empty list)</span>';
      // Array of primitives: render inline comma-separated
      if (val.every(function(v) { return typeof v !== 'object' || v === null; })) {
        return '<span style="color:var(--text)">' + val.map(function(v) { return esc(String(v)); }).join(', ') + '</span>';
      }
      // Array of objects: render as mini-tables
      return '<div style="display:flex;flex-direction:column;gap:4px;margin-top:2px">' + val.map(function(item, i) {
        return '<div style="padding:4px 6px;border-left:2px solid var(--border-ghost);margin-left:' + (depth * 8) + 'px">' + formatMemValue(item, depth + 1) + '</div>';
      }).join('') + '</div>';
    }
    // Object: render as label:data table
    var keys = Object.keys(val);
    if (keys.length === 0) return '<span style="color:var(--muted)">(empty object)</span>';
    var indent = depth > 0 ? 'margin-left:' + (depth * 8) + 'px;' : '';
    return '<table style="border-collapse:collapse;font-size:0.78rem;width:100%;' + indent + '">' + keys.map(function(k) {
      return '<tr style="border-bottom:1px solid var(--border-ghost)">'
        + '<td style="padding:3px 10px 3px 0;color:var(--accent);font-family:var(--mono);white-space:nowrap;vertical-align:top;font-size:0.75rem">' + esc(k) + '</td>'
        + '<td style="padding:3px 0;color:var(--text);word-break:break-word">' + formatMemValue(val[k], depth + 1) + '</td>'
        + '</tr>';
    }).join('') + '</table>';
  }
  /** Format plain text: extract embedded JSON blocks and render as tables, text as markdown. */
  function formatMemText(s) {
    // Split text into segments: plain text and embedded JSON blocks.
    // Detect JSON blocks by finding { or [ that open at a line boundary after a text prefix.
    var segments = [];
    var remaining = s;
    while (remaining.length > 0) {
      // Find the first top-level { or [ that starts a JSON block
      var jsonStart = -1;
      for (var ci = 0; ci < remaining.length; ci++) {
        var ch = remaining.charAt(ci);
        if (ch === '{' || ch === '[') {
          // Try to parse from here — greedy match for the longest valid JSON
          var depth2 = 0; var close = ch === '{' ? '}' : ']'; var inStr = false; var esc2 = false;
          for (var j = ci; j < remaining.length; j++) {
            var c2 = remaining.charAt(j);
            if (esc2) { esc2 = false; continue; }
            if (c2 === '\\' && inStr) { esc2 = true; continue; }
            if (c2 === '"') { inStr = !inStr; continue; }
            if (inStr) continue;
            if (c2 === ch) depth2++;
            if (c2 === close) { depth2--; if (depth2 === 0) {
              var candidate = remaining.substring(ci, j + 1);
              try { var parsed = JSON.parse(candidate); if (typeof parsed === 'object' && parsed !== null) { jsonStart = ci; break; } } catch(_) {}
              break;
            }}
          }
          if (jsonStart >= 0) break;
        }
      }
      if (jsonStart < 0) { segments.push({ type: 'text', value: remaining }); break; }
      // Extract text before JSON
      if (jsonStart > 0) segments.push({ type: 'text', value: remaining.substring(0, jsonStart) });
      // Extract JSON block
      var d3 = 0; var cl3 = remaining.charAt(jsonStart) === '{' ? '}' : ']'; var inS3 = false; var es3 = false; var end3 = jsonStart;
      for (var k = jsonStart; k < remaining.length; k++) {
        var c3 = remaining.charAt(k);
        if (es3) { es3 = false; continue; }
        if (c3 === '\\' && inS3) { es3 = true; continue; }
        if (c3 === '"') { inS3 = !inS3; continue; }
        if (inS3) continue;
        if (c3 === remaining.charAt(jsonStart)) d3++;
        if (c3 === cl3) { d3--; if (d3 === 0) { end3 = k + 1; break; } }
      }
      var jsonStr = remaining.substring(jsonStart, end3);
      try { segments.push({ type: 'json', value: JSON.parse(jsonStr) }); } catch(_) { segments.push({ type: 'text', value: jsonStr }); }
      remaining = remaining.substring(end3);
    }
    // Render segments
    var html = '<div style="font-size:0.8rem;line-height:1.45">';
    segments.forEach(function(seg) {
      if (seg.type === 'json') {
        html += '<div style="margin:6px 0;padding:6px 8px;border-left:2px solid var(--accent);background:rgba(255,255,255,0.02);border-radius:0 4px 4px 0">' + formatMemValue(seg.value, 0) + '</div>';
      } else {
        var text = esc(seg.value.trim());
        if (text.length === 0) return;
        text = text.replace(/\n/g, '<br>');
        text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
        html += '<div>' + text + '</div>';
      }
    });
    html += '</div>';
    // Truncate very long rendered content with expand toggle
    if (s.length > 1500) {
      var uid = 'mem-expand-' + Math.random().toString(36).substring(2, 8);
      return '<div>'
        + '<div id="' + uid + '-short" style="max-height:200px;overflow:hidden">' + html + '</div>'
        + '<span onclick="var sh=document.getElementById(\'' + uid + '-short\');var fl=document.getElementById(\'' + uid + '-full\');if(fl.style.display===\'none\'){fl.style.display=\'block\';sh.style.display=\'none\';this.textContent=\'\u25BC Show less\'}else{fl.style.display=\'none\';sh.style.display=\'block\';this.textContent=\'\u25B6 Show more\'}" style="font-size:0.72rem;color:var(--accent);cursor:pointer;display:inline-block;margin-top:4px">\u25B6 Show more</span>'
        + '<div id="' + uid + '-full" style="display:none">' + html + '</div>'
        + '</div>';
    }
    return html;
  }
  function uiIcon(name) {
    switch (name) {
      case 'refresh':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M13 5V2h-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 2A6 6 0 1 0 14 8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
      case 'download':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2v7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M5.5 7.5L8 10l2.5-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 12.5h11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
      case 'spark':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 1.5l1.7 3.4 3.8.6-2.7 2.6.6 3.7L8 10.3l-3.4 1.8.6-3.7L2.5 5.5l3.8-.6L8 1.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
      case 'power':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 1.8v5.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M11.5 3.7a5 5 0 1 1-7 0" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
      case 'trash':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2.5 4h11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M6 4V2.8h4V4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M4.2 4l.8 9h6l.8-9" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
      case 'plus':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
      case 'send':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8l12-5-3.5 10-2.7-4.2L2 8z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
      case 'search':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/><path d="M10.4 10.4L14 14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>';
      case 'back':
        return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M7 3L2.5 8 7 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
      default:
        return '';
    }
  }
  function uiBtnLabel(iconName, label) { return uiIcon(iconName) + '<span>' + label + '</span>'; }
  function copyIdBtn(id) { return '<button class="copy-id-btn" data-copy-id="' + esc(id) + '" title="Copy session ID"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="5" y="5" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.3"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></button>'; }
  function truncate(s, n) { return (s && s.length > n) ? s.slice(0, n) + '\u2026' : (s || ''); }
  function clamp01(v) {
    var n = Number(v);
    if (!isFinite(n)) return 0;
    if (n < 0) return 0;
    if (n > 1) return 1;
    return n;
  }
  function round2(v) {
    return Math.round(Number(v || 0) * 100) / 100;
  }
  function normalizeRoutingProfile(profile, preferredKey) {
    var p = {
      correctness: clamp01(profile && profile.correctness),
      cost: clamp01(profile && profile.cost),
      speed: clamp01(profile && profile.speed)
    };
    var keys = ['correctness', 'cost', 'speed'];
    var total = p.correctness + p.cost + p.speed;
    if (total <= 1.0 + 1e-9) {
      return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
    }
    if (preferredKey && keys.indexOf(preferredKey) !== -1) {
      var otherSum = 0;
      keys.forEach(function(k) { if (k !== preferredKey) otherSum += p[k]; });
      p[preferredKey] = Math.max(0, Math.min(p[preferredKey], 1 - otherSum));
      return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
    }
    if (total > 0) {
      var scale = 1 / total;
      p.correctness *= scale;
      p.cost *= scale;
      p.speed *= scale;
    }
    return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
  }
  function routingProfileTotal(profile) {
    return round2(clamp01(profile && profile.correctness) + clamp01(profile && profile.cost) + clamp01(profile && profile.speed));
  }
  function deriveRoutingProfile(cfg) {
    var correctness = clamp01(cfg && cfg.accuracy_floor);
    var costWeight = cfg && cfg.cost_weight;
    var cost = clamp01(costWeight == null ? (cfg && cfg.cost_aware ? 0.5 : 0.0) : costWeight);
    var confidence = clamp01(cfg && cfg.confidence_threshold != null ? cfg.confidence_threshold : 0.9);
    var speed = clamp01((0.95 - confidence) / 0.35);
    return normalizeRoutingProfile({
      correctness: round2(correctness),
      cost: round2(cost),
      speed: round2(speed)
    });
  }
  function projectRoutingPatchFromProfile(profile) {
    var p = {
      correctness: clamp01(profile && profile.correctness),
      cost: clamp01(profile && profile.cost),
      speed: clamp01(profile && profile.speed)
    };
    return {
      models: {
        routing: {
          accuracy_floor: round2(p.correctness),
          cost_aware: p.cost > 0.01,
          cost_weight: round2(p.cost),
          confidence_threshold: round2(0.95 - (0.35 * p.speed)),
          estimated_output_tokens: Math.max(200, Math.min(1200, Math.round(1200 - (800 * p.speed))))
        }
      }
    };
  }
  function normalizeContextBudget(budget) {
    var levels = ['L0', 'L1', 'L2', 'L3'];
    var normalized = {
      l0: Math.max(1000, parseInt(budget && budget.l0, 10) || 4000),
      l1: Math.max(2000, parseInt(budget && budget.l1, 10) || 8000),
      l2: Math.max(4000, parseInt(budget && budget.l2, 10) || 16000),
      l3: Math.max(8000, parseInt(budget && budget.l3, 10) || 32000),
      channel_minimum: String((budget && budget.channel_minimum) || 'L1').toUpperCase()
    };
    if (levels.indexOf(normalized.channel_minimum) === -1) normalized.channel_minimum = 'L1';
    return normalized;
  }
  function renderRoutingSpiderSvg(profile) {
    var values = [clamp01(profile.correctness), clamp01(profile.cost), clamp01(profile.speed)];
    var labels = ['Correctness', 'Cost', 'Speed'];
    var cx = 110, cy = 110, r = 78;
    var grid = '';
    [0.25, 0.5, 0.75, 1.0].forEach(function(level) {
      var pts = [];
      for (var i = 0; i < 3; i++) {
        var angle = (-Math.PI / 2) + (i * (2 * Math.PI / 3));
        pts.push((cx + Math.cos(angle) * r * level).toFixed(1) + ',' + (cy + Math.sin(angle) * r * level).toFixed(1));
      }
      grid += '<polygon points="' + pts.join(' ') + '" fill="none" stroke="var(--border)" stroke-width="1"/>';
    });
    var axes = '';
    var labelEls = '';
    var polyPts = [];
    for (var i = 0; i < 3; i++) {
      var ang = (-Math.PI / 2) + (i * (2 * Math.PI / 3));
      var ox = cx + Math.cos(ang) * r;
      var oy = cy + Math.sin(ang) * r;
      axes += '<line x1="' + cx + '" y1="' + cy + '" x2="' + ox.toFixed(1) + '" y2="' + oy.toFixed(1) + '" stroke="var(--border)"/>';
      var lx = cx + Math.cos(ang) * (r + 18);
      var ly = cy + Math.sin(ang) * (r + 18);
      labelEls += '<text x="' + lx.toFixed(1) + '" y="' + ly.toFixed(1) + '" text-anchor="middle" fill="var(--muted)" style="font-size:10px;font-family:var(--font)">' + labels[i] + '</text>';
      var px = cx + Math.cos(ang) * r * values[i];
      var py = cy + Math.sin(ang) * r * values[i];
      polyPts.push(px.toFixed(1) + ',' + py.toFixed(1));
    }
    return '<svg viewBox="0 0 220 220" width="220" height="220" aria-label="routing profile spider graph">'
      + grid + axes
      + '<polygon points="' + polyPts.join(' ') + '" fill="rgba(193,128,255,0.25)" stroke="var(--accent)" stroke-width="2"/>'
      + labelEls
      + '</svg>';
  }
  function renderModelDecisionGraph(events, focusTurnId, focusModel, focusEdge) {
    var MAX_GRAPH_CANDIDATES = 18;
    var rows = Array.isArray(events) ? events.slice() : [];
    if (!rows.length) {
      return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Task Model Decision Graph</div><div style="color:var(--muted)">No model selection events yet.</div></div>';
    }
    var byTurn = {};
    rows.forEach(function(ev) {
      var tid = String((ev && ev.turn_id) || '').trim();
      if (!tid) return;
      byTurn[tid] = ev;
    });
    var selectedTurn = focusTurnId && byTurn[focusTurnId] ? focusTurnId : String(rows[0].turn_id || '');
    var selectedEvent = byTurn[selectedTurn] || rows[0] || {};
    var selectedModel = String(selectedEvent.selected_model || '').trim();
    var candidates = Array.isArray(selectedEvent.candidates) ? selectedEvent.candidates : [];

    var nodeMap = {};
    candidates.forEach(function(c) {
      var model = String((c && c.model) || '').trim();
      if (!model) return;
      var score = typeof c.metascore === 'number' ? c.metascore : null;
      nodeMap[model] = {
        model: model,
        score: score,
        usable: c && c.usable !== false
      };
    });
    if (selectedModel && !nodeMap[selectedModel]) {
      nodeMap[selectedModel] = { model: selectedModel, score: null, usable: true };
    }
    var sortedNodes = Object.keys(nodeMap).sort(function(a, b) {
      if (a === selectedModel) return -1;
      if (b === selectedModel) return 1;
      var sa = nodeMap[a] && typeof nodeMap[a].score === 'number' ? nodeMap[a].score : -Infinity;
      var sb = nodeMap[b] && typeof nodeMap[b].score === 'number' ? nodeMap[b].score : -Infinity;
      if (sa !== sb) return sb - sa;
      return a.localeCompare(b);
    });
    var hiddenCount = Math.max(0, sortedNodes.length - MAX_GRAPH_CANDIDATES);
    var nodes = sortedNodes.slice(0, MAX_GRAPH_CANDIDATES);
    if (selectedModel && nodes.indexOf(selectedModel) === -1) {
      nodes[nodes.length - 1] = selectedModel;
    }
    if (!nodes.length) return '';

    var width = 680, height = 320;
    var selectedX = 530, selectedY = height / 2;
    var candidateX = 220;
    var pos = {};
    var candidateModels = nodes.filter(function(model) { return model !== selectedModel; });
    if (selectedModel) pos[selectedModel] = { x: selectedX, y: selectedY };
    candidateModels.forEach(function(model, idx) {
      var y;
      if (candidateModels.length === 1) {
        y = selectedY;
      } else {
        var pad = 28;
        var span = Math.max(10, (height - (pad * 2)));
        y = pad + ((span * idx) / (candidateModels.length - 1));
      }
      pos[model] = { x: candidateX, y: y };
    });

    var edges = {};
    candidateModels.forEach(function(model) {
      if (!selectedModel || model === selectedModel) return;
      var key = model + '→' + selectedModel;
      edges[key] = { key: key, from: model, to: selectedModel, count: 1 };
    });
    var edgeKeys = Object.keys(edges);
    var edgeEls = edgeKeys.map(function(k) {
      var e = edges[k];
      var a = pos[e.from], b = pos[e.to];
      if (!a || !b) return '';
      var active = focusEdge && focusEdge === e.key;
      var faded = (focusModel && e.from !== focusModel && e.to !== focusModel) || (focusEdge && !active);
      var cls = 'model-graph-edge' + (active ? ' active' : '') + (faded ? ' model-graph-faded' : '');
      return '<line class="' + cls + '" data-edge-key="' + esc(e.key) + '" x1="' + a.x.toFixed(1) + '" y1="' + a.y.toFixed(1) + '" x2="' + b.x.toFixed(1) + '" y2="' + b.y.toFixed(1) + '" stroke-width="1.4"><title>' + esc(e.key) + '</title></line>';
    }).join('');

    var laneGuides = '';
    if (candidateModels.length > 1) {
      var yMin = pos[candidateModels[0]].y;
      var yMax = pos[candidateModels[candidateModels.length - 1]].y;
      laneGuides = '<line x1="' + candidateX + '" y1="' + yMin.toFixed(1) + '" x2="' + candidateX + '" y2="' + yMax.toFixed(1) + '" stroke="var(--border)" stroke-opacity="0.45" stroke-dasharray="2 3" />';
    }

    var nodeEls = nodes.map(function(model) {
      var p = pos[model];
      if (!p) return '';
      var active = (focusModel && focusModel === model) || (!focusModel && model === selectedModel);
      var faded = focusModel && !active;
      var unusable = nodeMap[model] && nodeMap[model].usable === false;
      var cls = 'model-graph-node' + (active ? ' active' : '') + (faded ? ' model-graph-faded' : '') + (unusable ? ' model-graph-node-unusable' : '');
      var short = truncate(model.split('/').pop() || model, 22);
      var score = nodeMap[model].score;
      var scoreTxt = typeof score === 'number' ? ' · score ' + score.toFixed(3) : '';
      var selectedTxt = model === selectedModel ? ' · selected' : '';
      var r = model === selectedModel ? 14 : 10;
      var labelX = model === selectedModel ? p.x : (p.x - 14);
      var labelY = model === selectedModel ? (p.y + 28) : (p.y + 3);
      var anchor = model === selectedModel ? 'middle' : 'end';
      return '<g data-node-model="' + esc(model) + '" style="cursor:pointer">'
        + '<circle class="' + cls + '" cx="' + p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="' + r + '"><title>' + esc(model + selectedTxt + scoreTxt) + '</title></circle>'
        + '<text class="model-graph-node-label" text-anchor="' + anchor + '" x="' + labelX.toFixed(1) + '" y="' + labelY.toFixed(1) + '">' + esc(short) + '</text>'
        + '</g>';
    }).join('');

    var options = rows.slice(0, 80).map(function(ev) {
      var tid = String(ev.turn_id || '');
      var stamp = String(ev.created_at || '').replace('T', ' ').replace('Z', '');
      var excerpt = truncate(String(ev.user_excerpt || ''), 44);
      var label = truncate(tid, 8) + (excerpt ? ' · ' + excerpt : '') + (stamp ? ' · ' + stamp : '');
      return '<option value="' + esc(tid) + '"' + (tid === selectedTurn ? ' selected' : '') + '>' + esc(label) + '</option>';
    }).join('');

    var detail = 'Task turn: ' + selectedTurn + '. Selected model: ' + (selectedModel || 'unknown') + '. Displaying top ' + nodes.length + ' candidate models' + (hiddenCount > 0 ? ' (' + hiddenCount + ' hidden).' : '.');
    if (focusEdge && edges[focusEdge]) {
      detail = 'Task turn: ' + selectedTurn + '. Candidate edge: ' + edges[focusEdge].from + ' → ' + edges[focusEdge].to + '.';
    } else if (focusModel) {
      var n = nodeMap[focusModel];
      if (n) detail = 'Task turn: ' + selectedTurn + '. Focused model: ' + focusModel + (typeof n.score === 'number' ? ' (metascore ' + n.score.toFixed(3) + ').' : '.');
    }

    return '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem">'
      + '<div class="card-title">Task Model Decision Graph</div>'
      + '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.5rem">Candidate models and chosen model for a single task. Pick a task turn to inspect that decision.</div>'
      + '<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.55rem"><label for="model-graph-task-select" style="font-size:0.75rem;color:var(--muted)">Task turn</label><select id="model-graph-task-select" class="select" style="min-width:320px;max-width:100%">' + options + '</select></div>'
      + '<div class="model-graph-wrap">'
      + '<svg class="model-graph-svg" viewBox="0 0 680 320" preserveAspectRatio="xMidYMid meet">'
      + '<text x="36" y="22" fill="var(--muted)" style="font-size:10px;font-family:var(--font-mono)">Candidates (ranked)</text>'
      + '<text x="487" y="22" fill="var(--muted)" style="font-size:10px;font-family:var(--font-mono)">Selected</text>'
      + laneGuides + edgeEls + nodeEls
      + '</svg></div>'
      + '<div id="model-graph-detail" class="model-graph-detail">' + esc(detail) + '</div>'
      + '<div style="margin-top:0.5rem"><button class="btn secondary" id="model-graph-clear-focus" style="font-size:0.6875rem;padding:0.22rem 0.55rem">Clear focus</button></div>'
      + '</div>';
  }
  var AGENT_DISPLAY_NAME = '';
  function setAgentDisplayName(name) {
    var value = String(name || '').trim();
    if (value) AGENT_DISPLAY_NAME = value;
  }
  function sessionAssistantLabel(session, fallback) {
    var fallbackLabel = fallback || 'assistant';
    if (!session) return AGENT_DISPLAY_NAME || fallbackLabel;
    var candidate = (session.agent_name || session.agent_id || '').toString().trim();
    if (candidate && candidate.toLowerCase() !== 'default') return candidate;
    if (AGENT_DISPLAY_NAME) return AGENT_DISPLAY_NAME;
    return fallbackLabel;
  }
  function sanitizeMarkdownUrl(url) {
    var raw = String(url || '').trim();
    if (!raw) return null;
    var lowered = raw.toLowerCase();
    if (lowered.indexOf('javascript:') === 0 || lowered.indexOf('data:') === 0 || lowered.indexOf('vbscript:') === 0) return null;
    if (lowered.indexOf('http://') === 0 || lowered.indexOf('https://') === 0 || lowered.indexOf('mailto:') === 0) return raw;
    if (raw.charAt(0) === '/' || raw.charAt(0) === '#') return raw;
    return null;
  }
  function renderInlineMarkdown(text) {
    var src = String(text || '');
    if (!src) return '';
    var html = esc(src);
    var inlineCodes = [];
    html = html.replace(/`([^`]+)`/g, function(_, code) {
      var token = '\x01IC' + inlineCodes.length + '\x01';
      inlineCodes.push('<code>' + code + '</code>');
      return token;
    });
    html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, label, url) {
      var safe = sanitizeMarkdownUrl(url);
      var safeLabel = renderInlineMarkdown(label);
      if (!safe) return safeLabel;
      return '<a href="' + esc(safe) + '" target="_blank" rel="noopener noreferrer">' + safeLabel + '</a>';
    });
    html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
    html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
    html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
    html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
    html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
    html = html.replace(/~([^~\n]+)~/g, '<del>$1</del>');
    html = html.replace(/\x01IC(\d+)\x01/g, function(_, idx) {
      var n = Number(idx);
      return inlineCodes[n] || '';
    });
    return html;
  }
  function renderSafeMarkdownImpl(input) {
    var src = String(input || '');
    if (!src) return '';
    var codeBlocks = [];
    src = src.replace(/```([^\n`]*)\n?([\s\S]*?)```/g, function(_, lang, code) {
      var token = '__MD_CODE_BLOCK_' + codeBlocks.length + '__';
      var language = String(lang || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
      var classAttr = language ? ' class="language-' + language + '"' : '';
      codeBlocks.push('<pre><code' + classAttr + '>' + esc(code) + '</code></pre>');
      return token;
    });
    var lines = src.replace(/\r\n/g, '\n').split('\n');
    var htmlParts = [];
    var paragraph = [];
    var inUl = false;
    var inOl = false;
    var inQuote = false;
    function flushParagraph() {
      if (!paragraph.length) return;
      htmlParts.push('<p>' + paragraph.map(renderInlineMarkdown).join('<br>') + '</p>');
      paragraph = [];
    }
    function closeLists() {
      if (inUl) { htmlParts.push('</ul>'); inUl = false; }
      if (inOl) { htmlParts.push('</ol>'); inOl = false; }
    }
    function closeQuote() {
      if (inQuote) { htmlParts.push('</blockquote>'); inQuote = false; }
    }
    for (var i = 0; i < lines.length; i++) {
      var raw = lines[i];
      var trimmed = raw.trim();
      var tokenOnly = /^__MD_CODE_BLOCK_\d+__$/.test(trimmed);
      if (!trimmed) {
        flushParagraph();
        closeLists();
        closeQuote();
        continue;
      }
      if (tokenOnly) {
        flushParagraph();
        closeLists();
        closeQuote();
        htmlParts.push(trimmed);
        continue;
      }
      var headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
      if (headingMatch) {
        flushParagraph();
        closeLists();
        closeQuote();
        var level = headingMatch[1].length;
        htmlParts.push('<h' + level + '>' + renderInlineMarkdown(headingMatch[2]) + '</h' + level + '>');
        continue;
      }
      if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
        flushParagraph();
        closeLists();
        closeQuote();
        htmlParts.push('<hr>');
        continue;
      }
      // Table: | col | col | with separator line following
      if (trimmed.charAt(0) === '|' && i + 1 < lines.length && /^\|[\s\-:|]+\|$/.test(lines[i + 1].trim())) {
        flushParagraph();
        closeLists();
        closeQuote();
        var headerCells = trimmed.split('|').slice(1, -1).map(function(c) { return c.trim(); });
        var tbl = '<table class="md-table"><thead><tr>';
        for (var h = 0; h < headerCells.length; h++) {
          tbl += '<th>' + renderInlineMarkdown(headerCells[h]) + '</th>';
        }
        tbl += '</tr></thead><tbody>';
        i += 2; // skip header + separator
        while (i < lines.length && lines[i].trim().charAt(0) === '|') {
          var cells = lines[i].trim().split('|').slice(1, -1).map(function(c) { return c.trim(); });
          tbl += '<tr>';
          for (var c = 0; c < cells.length; c++) {
            tbl += '<td>' + renderInlineMarkdown(cells[c]) + '</td>';
          }
          tbl += '</tr>';
          i++;
        }
        tbl += '</tbody></table>';
        htmlParts.push(tbl);
        i--; // compensate for the for-loop i++
        continue;
      }
      var quoteMatch = trimmed.match(/^>\s?(.*)$/);
      if (quoteMatch) {
        flushParagraph();
        closeLists();
        if (!inQuote) { htmlParts.push('<blockquote>'); inQuote = true; }
        htmlParts.push('<p>' + renderInlineMarkdown(quoteMatch[1]) + '</p>');
        continue;
      }
      closeQuote();
      var ulMatch = trimmed.match(/^[-*+]\s+(.+)$/);
      if (ulMatch) {
        flushParagraph();
        if (inOl) { htmlParts.push('</ol>'); inOl = false; }
        if (!inUl) { htmlParts.push('<ul>'); inUl = true; }
        htmlParts.push('<li>' + renderInlineMarkdown(ulMatch[1]) + '</li>');
        continue;
      }
      var olMatch = trimmed.match(/^\d+\.\s+(.+)$/);
      if (olMatch) {
        flushParagraph();
        if (inUl) { htmlParts.push('</ul>'); inUl = false; }
        if (!inOl) { htmlParts.push('<ol>'); inOl = true; }
        htmlParts.push('<li>' + renderInlineMarkdown(olMatch[1]) + '</li>');
        continue;
      }
      closeLists();
      paragraph.push(trimmed);
    }
    flushParagraph();
    closeLists();
    closeQuote();
    var html = htmlParts.join('\n');
    html = html.replace(/__MD_CODE_BLOCK_(\d+)__/g, function(_, idx) {
      var n = Number(idx);
      return codeBlocks[n] || '';
    });
    return html;
  }
  /** Wraps markdown render so a pathological assistant payload cannot blank the session UI. */
  function renderSafeMarkdown(input) {
    try {
      return renderSafeMarkdownImpl(input);
    } catch (err) {
      if (typeof console !== 'undefined' && console.error) { console.error('renderSafeMarkdown', err); }
      var full = String(input || '');
      var snippet = full.slice(0, 1200);
      return '<p class="md-render-fallback" style="color:var(--warning);font-size:0.8125rem">This message could not be formatted safely for the dashboard. Showing escaped source:</p>'
        + '<pre style="white-space:pre-wrap;word-break:break-word;font-size:0.75rem;max-height:14rem;overflow:auto;border:1px solid var(--border);padding:0.5rem;border-radius:4px">'
        + esc(snippet) + (full.length > 1200 ? '\n…' : '') + '</pre>';
    }
  }
