Newer
Older
navi-1 / debug / index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Navi — Debug</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      --bg:       #0e0e0e;
      --bg2:      #161616;
      --bg3:      #1e1e1e;
      --bg4:      #222;
      --border:   #252525;
      --border2:  #2e2e2e;
      --text:     #d4d4d4;
      --text2:    #888;
      --text3:    #555;
      --accent:   #4ec9b0;

      --c-system:    #4ec9b0;
      --c-user:      #569cd6;
      --c-assistant: #c586c0;
      --c-tool:      #ce9178;
    }

    html, body { height: 100%; }
    body {
      font-family: ui-monospace, "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
      font-size: 13px;
      background: var(--bg);
      color: var(--text);
      display: flex;
      flex-direction: column;
      height: 100vh;
      overflow: hidden;
    }

    /* ── Header ── */
    header {
      flex-shrink: 0;
      background: var(--bg2);
      border-bottom: 1px solid var(--border);
      padding: 0 14px;
      display: flex;
      align-items: stretch;
      gap: 10px;
    }

    .logo {
      font-size: 11px;
      color: var(--text3);
      letter-spacing: .1em;
      text-transform: uppercase;
      white-space: nowrap;
      align-self: center;
      padding: 10px 0;
    }
    .logo b { color: var(--accent); }

    /* ── Tabs ── */
    .tabs {
      display: flex;
      align-items: stretch;
      gap: 2px;
      margin-left: 10px;
    }
    .tab {
      padding: 0 14px;
      font-family: inherit;
      font-size: 12px;
      color: var(--text3);
      background: none;
      border: none;
      border-bottom: 2px solid transparent;
      cursor: pointer;
      transition: color .15s, border-color .15s;
    }
    .tab:hover { color: var(--text2); }
    .tab.active { color: var(--text); border-bottom-color: var(--accent); }

    .header-right {
      margin-left: auto;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .header-right a {
      color: var(--text2);
      text-decoration: none;
      font-size: 12px;
    }
    .header-right a:hover { color: var(--text); }

    input, button, select {
      font-family: inherit;
      font-size: 12px;
      background: var(--bg3);
      border: 1px solid var(--border2);
      color: var(--text);
      border-radius: 4px;
      outline: none;
    }
    input  { padding: 4px 8px; }
    input:focus { border-color: #555; }
    button { padding: 4px 10px; cursor: pointer; }
    button:hover { background: var(--bg4); border-color: #555; }
    button.active { background: #1e3a2f; border-color: var(--accent); color: var(--accent); }

    #stats { font-size: 11px; color: var(--text3); white-space: nowrap; }
    #stats b { color: var(--text2); }

    /* ── Layout ── */
    .layout {
      display: flex;
      flex: 1;
      overflow: hidden;
    }

    /* ── Sidebar ── */
    aside {
      width: 220px;
      flex-shrink: 0;
      background: var(--bg2);
      border-right: 1px solid var(--border);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .aside-title {
      font-size: 10px;
      color: var(--text3);
      text-transform: uppercase;
      letter-spacing: .08em;
      padding: 8px 10px 6px;
      border-bottom: 1px solid var(--border);
      flex-shrink: 0;
    }
    .aside-scroll { flex: 1; overflow-y: auto; }

    .list-item {
      padding: 7px 10px;
      cursor: pointer;
      border-bottom: 1px solid var(--border);
      transition: background .1s;
    }
    .list-item:hover { background: var(--bg3); }
    .list-item.selected { background: #1a1a2a; border-left: 2px solid var(--c-user); padding-left: 8px; }

    .li-name  { font-size: 11px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .li-meta  { font-size: 10px; color: var(--text3); margin-top: 2px; display: flex; gap: 6px; flex-wrap: wrap; }
    .li-profile { color: var(--text2); }
    .li-pin   { color: var(--accent); }

    /* ── Main ── */
    main {
      flex: 1;
      overflow-y: auto;
      padding: 14px 16px;
    }

    #placeholder { text-align: center; color: var(--text3); padding: 80px 0; font-size: 12px; }
    #error-msg   { color: #f48771; padding: 20px; text-align: center; }

    /* ── Context: message cards ── */
    .msg {
      display: grid;
      grid-template-columns: 86px 1fr;
      margin-bottom: 10px;
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
    }
    .msg-role {
      background: var(--bg2);
      border-right: 1px solid var(--border);
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 8px 5px;
      gap: 4px;
      text-align: center;
    }
    .role-badge {
      font-size: 9px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
      padding: 2px 6px; border-radius: 3px;
    }
    .role-system    { background: #1e3a2f; color: var(--c-system); }
    .role-user      { background: #1e2d40; color: var(--c-user); }
    .role-assistant { background: #2d1e3a; color: var(--c-assistant); }
    .role-tool      { background: #3a2d1e; color: var(--c-tool); }

    .tag { font-size: 9px; padding: 1px 5px; border-radius: 3px; }
    .tag-summary { background: #3a3a1e; color: #dcdcaa; }
    .tag-plan    { background: #1e2d1e; color: #6a9955; }
    .tag-images  { background: #1e3a3a; color: var(--c-system); }
    .tag-tools   { background: #3a1e2d; color: #f48771; }
    .tag-think   { background: #2d2d1e; color: #dcdcaa; }

    .msg-idx { font-size: 10px; color: var(--text3); }
    .msg-body { padding: 8px 12px; overflow: hidden; min-width: 0; }

    .section-label {
      font-size: 10px; color: var(--text3);
      text-transform: uppercase; letter-spacing: .06em;
      margin: 8px 0 4px;
    }

    pre.content {
      white-space: pre-wrap; word-break: break-word;
      line-height: 1.55; color: var(--text);
      font-family: inherit; font-size: 12px;
    }
    pre.content.dim { color: var(--text2); }
    pre.content.thinking-text {
      color: #dcdcaa; background: #1e1e0e;
      padding: 6px 8px; border-radius: 4px;
      border-left: 2px solid #5a5a20;
    }

    .tool-call-block {
      background: var(--bg2); border: 1px solid var(--border2);
      border-radius: 4px; margin-bottom: 6px; overflow: hidden;
    }
    .tool-call-name { background: var(--bg4); color: var(--c-tool); padding: 2px 8px; font-size: 11px; font-weight: 600; }
    .tool-call-args { padding: 5px 8px; white-space: pre-wrap; word-break: break-word; color: #9cdcfe; font-size: 11px; font-family: inherit; }

    .tool-id   { font-size: 10px; color: var(--text3); margin-top: 4px; }
    .char-count{ font-size: 10px; color: var(--text3); margin-top: 6px; }

    .img-strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
    .img-thumb { width: 72px; height: 72px; object-fit: cover; border-radius: 3px; border: 1px solid var(--border2); cursor: pointer; }

    /* ── Prompts tab ── */
    .prompt-card {
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
      margin-bottom: 12px;
    }
    .prompt-section {
      border-bottom: 1px solid var(--border);
    }
    .prompt-section:last-child { border-bottom: none; }
    .prompt-section-header {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 6px 12px;
      background: var(--bg2);
      cursor: pointer;
      user-select: none;
    }
    .prompt-section-header:hover { background: var(--bg3); }
    .ps-label {
      font-size: 10px; font-weight: 700; text-transform: uppercase;
      letter-spacing: .07em;
    }
    .ps-label.lbl-persona  { color: var(--accent); }
    .ps-label.lbl-profile  { color: var(--c-assistant); }
    .ps-label.lbl-profiles { color: var(--c-user); }
    .ps-chars { font-size: 10px; color: var(--text3); margin-left: auto; }
    .ps-chevron { font-size: 11px; color: var(--text3); transition: transform .15s; }
    .ps-chevron.open { transform: rotate(90deg); }

    .prompt-section-body {
      padding: 10px 12px;
      background: var(--bg);
    }
    .prompt-section-body pre {
      white-space: pre-wrap; word-break: break-word;
      line-height: 1.6; color: var(--text); font-family: inherit; font-size: 12px;
    }
    .prompt-section-body.hidden { display: none; }

    .profile-header {
      display: flex; align-items: center; gap: 10px;
      padding: 8px 12px;
      background: var(--bg2);
      border-bottom: 1px solid var(--border2);
    }
    .profile-header h2 { font-size: 13px; font-weight: 600; color: var(--text); }
    .ph-model { font-size: 11px; color: var(--text3); margin-left: auto; }
    .ph-chars { font-size: 11px; color: var(--text2); }
    .ph-tools-badge {
      font-size: 10px; padding: 1px 6px; border-radius: 3px;
      background: #1e2d1e; color: #6a9955; cursor: pointer;
    }
    .ph-tools-badge:hover { background: #253d25; }

    .tools-inline {
      padding: 6px 12px 8px;
      background: var(--bg);
      border-bottom: 1px solid var(--border);
      display: flex; flex-wrap: wrap; gap: 5px;
    }
    .tools-inline.hidden { display: none; }
    .tool-pill {
      font-size: 10px; padding: 2px 7px; border-radius: 3px;
      background: var(--bg3); border: 1px solid var(--border2); color: var(--text2);
    }

    /* ── Tools tab ── */
    .tool-card {
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
      margin-bottom: 10px;
    }
    .tool-card-header {
      display: flex; align-items: center; gap: 8px;
      padding: 7px 12px;
      background: var(--bg2);
      cursor: pointer;
      user-select: none;
    }
    .tool-card-header:hover { background: var(--bg3); }
    .tool-name { font-size: 12px; font-weight: 700; color: var(--c-tool); }
    .tool-builtin { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #1e3a2f; color: var(--accent); }
    .tool-user    { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #2d1e3a; color: var(--c-assistant); }
    .tool-chevron { font-size: 11px; color: var(--text3); margin-left: auto; transition: transform .15s; }
    .tool-chevron.open { transform: rotate(90deg); }

    .tool-card-body { padding: 10px 12px; background: var(--bg); }
    .tool-card-body.hidden { display: none; }
    .tool-desc { color: var(--text2); line-height: 1.55; font-size: 12px; margin-bottom: 10px; white-space: pre-wrap; word-break: break-word; }

    .params-label { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 6px; }
    .param-row {
      display: grid; grid-template-columns: 140px 80px 1fr;
      gap: 0 10px; padding: 4px 0;
      border-top: 1px solid var(--border);
      font-size: 11px; line-height: 1.4;
    }
    .param-name { color: #9cdcfe; }
    .param-type { color: #ce9178; }
    .param-desc { color: var(--text2); word-break: break-word; }
    .param-req   { color: #f48771; font-size: 9px; margin-left: 3px; }

    /* ── Planning tab ── */
    .plan-run {
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
      margin-bottom: 10px;
    }
    .plan-run-header {
      display: flex; align-items: center; gap: 10px;
      padding: 7px 12px;
      background: var(--bg2);
      cursor: pointer;
      user-select: none;
    }
    .plan-run-header:hover { background: var(--bg3); }
    .plan-run-ts { font-size: 11px; color: var(--text2); }
    .plan-run-result {
      font-size: 10px; padding: 1px 6px; border-radius: 3px; font-weight: 700;
    }
    .result-plan   { background: #1e2d1e; color: #6a9955; }
    .result-direct { background: #3a3a1e; color: #dcdcaa; }
    .result-other  { background: #3a1e1e; color: #f48771; }
    .plan-run-chevron { font-size: 11px; color: var(--text3); margin-left: auto; transition: transform .15s; }
    .plan-run-chevron.open { transform: rotate(90deg); }

    .plan-run-body { background: var(--bg); }
    .plan-run-body.hidden { display: none; }

    .phase-block {
      border-top: 1px solid var(--border);
    }
    .phase-header {
      display: flex; align-items: center; gap: 8px;
      padding: 5px 12px;
      background: var(--bg2);
      cursor: pointer;
      user-select: none;
    }
    .phase-header:hover { background: var(--bg3); }
    .phase-label {
      font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
    }
    .phase-1 { color: var(--accent); }
    .phase-2 { color: var(--c-assistant); }
    .phase-3 { color: var(--c-user); }
    .phase-tokens { font-size: 10px; color: var(--text3); margin-left: auto; }
    .phase-changed { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #1e3a2f; color: var(--accent); }
    .phase-chevron { font-size: 11px; color: var(--text3); transition: transform .15s; }
    .phase-chevron.open { transform: rotate(90deg); }

    .phase-body { padding: 10px 12px; background: var(--bg); }
    .phase-body.hidden { display: none; }
    .phase-body pre {
      white-space: pre-wrap; word-break: break-word;
      line-height: 1.55; color: var(--text); font-family: inherit; font-size: 12px;
    }
    .advisor-block {
      border-left: 2px solid var(--accent2);
      margin-bottom: 10px;
      padding-left: 10px;
    }
    .advisor-header {
      display: flex; align-items: center; gap: 8px;
      margin-bottom: 4px; font-size: 12px; color: var(--text-dim);
    }

    /* ── Scrollbar ── */
    ::-webkit-scrollbar { width: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
    ::-webkit-scrollbar-thumb:hover { background: #444; }

    .hidden { display: none !important; }
  </style>
</head>
<body>

<header>
  <div class="logo"><b>navi</b> debug</div>

  <div class="tabs">
    <button class="tab active" data-tab="context">Context</button>
    <button class="tab" data-tab="planning">Planning</button>
    <button class="tab" data-tab="prompts">Prompts</button>
    <button class="tab" data-tab="tools">Tools</button>
  </div>

  <!-- Context tab controls -->
  <div id="ctx-controls" class="header-right">
    <input id="id-input" type="text" placeholder="session id…" spellcheck="false" style="width:260px">
    <button id="btn-load">Load</button>
    <button id="btn-refresh" title="Reload">↺</button>
    <button id="btn-auto" title="Auto-refresh 3s">Auto</button>
    <a href="/#/admin">Admin</a>
    <a href="/debug/eval">Eval</a>
    <div id="stats"></div>
  </div>

  <!-- Prompts / Tools tab controls -->
  <div id="pt-controls" class="header-right hidden">
    <a href="/#/admin">Admin</a>
    <a href="/debug/eval">Eval</a>
    <button id="btn-reload-pt" title="Reload">↺</button>
  </div>
</header>

<div class="layout">

  <!-- ── Context sidebar ── -->
  <aside id="ctx-aside">
    <div class="aside-title">Sessions</div>
    <div class="aside-scroll" id="session-list">
      <div style="padding:12px;color:#555;font-size:11px;">loading…</div>
    </div>
  </aside>

  <!-- ── Prompts sidebar ── -->
  <aside id="prompts-aside" class="hidden">
    <div class="aside-title">Profiles</div>
    <div class="aside-scroll" id="profile-list"></div>
  </aside>

  <!-- ── Planning sidebar (reuses session list) ── -->
  <aside id="planning-aside" class="hidden">
    <div class="aside-title">Sessions</div>
    <div class="aside-scroll" id="planning-session-list">
      <div style="padding:12px;color:#555;font-size:11px;">loading…</div>
    </div>
  </aside>

  <!-- ── Tools sidebar ── -->
  <aside id="tools-aside" class="hidden">
    <div class="aside-title">Filter</div>
    <div style="padding:8px;">
      <input id="tool-filter" type="text" placeholder="search…" style="width:100%">
    </div>
  </aside>

  <main id="main">
    <!-- Context view -->
    <div id="view-context">
      <div id="placeholder">Select a session or enter an ID above.</div>
      <div id="error-msg" hidden></div>
      <div id="messages"></div>
    </div>

    <!-- Planning view -->
    <div id="view-planning" class="hidden">
      <div id="planning-placeholder" style="text-align:center;color:#555;padding:80px 0;font-size:12px;">Select a session to view planning logs.</div>
      <div id="planning-content"></div>
    </div>

    <!-- Prompts view -->
    <div id="view-prompts" class="hidden">
      <div id="prompts-placeholder" style="text-align:center;color:#555;padding:80px 0;font-size:12px;">Select a profile.</div>
      <div id="prompts-content"></div>
    </div>

    <!-- Tools view -->
    <div id="view-tools" class="hidden">
      <div id="tools-content"></div>
    </div>
  </main>
</div>

<script>
// ═══════════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════════
let currentTab          = 'context'
let currentSession      = null
let currentPlanSession  = null
let currentProfile      = null
let autoTimer           = null
let allTools            = []
let allPrompts          = []

// ═══════════════════════════════════════════════════════════
// DOM refs
// ═══════════════════════════════════════════════════════════
const idInput      = document.getElementById('id-input')
const btnLoad      = document.getElementById('btn-load')
const btnRefresh   = document.getElementById('btn-refresh')
const btnAuto      = document.getElementById('btn-auto')
const btnReloadPt  = document.getElementById('btn-reload-pt')
const statsEl      = document.getElementById('stats')
const msgsEl       = document.getElementById('messages')
const errEl        = document.getElementById('error-msg')
const phEl         = document.getElementById('placeholder')
const sessionListEl= document.getElementById('session-list')
const profileListEl= document.getElementById('profile-list')
const toolFilterEl = document.getElementById('tool-filter')
const promptsContent = document.getElementById('prompts-content')
const promptsPh    = document.getElementById('prompts-placeholder')
const toolsContent = document.getElementById('tools-content')

// ═══════════════════════════════════════════════════════════
// Tab switching
// ═══════════════════════════════════════════════════════════
function switchTab(tab) {
  currentTab = tab
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab))

  setHidden('ctx-aside',      tab !== 'context')
  setHidden('planning-aside', tab !== 'planning')
  setHidden('prompts-aside',  tab !== 'prompts')
  setHidden('tools-aside',    tab !== 'tools')

  setHidden('view-context',   tab !== 'context')
  setHidden('view-planning',  tab !== 'planning')
  setHidden('view-prompts',   tab !== 'prompts')
  setHidden('view-tools',     tab !== 'tools')

  setHidden('ctx-controls', tab !== 'context' && tab !== 'planning')
  setHidden('pt-controls',  tab === 'context' || tab === 'planning')

  if (tab === 'planning') loadPlanningSessions()
}

document.querySelectorAll('.tab').forEach(t =>
  t.addEventListener('click', () => switchTab(t.dataset.tab))
)

function setHidden(id, hidden) {
  document.getElementById(id).classList.toggle('hidden', hidden)
}

// ═══════════════════════════════════════════════════════════
// Init
// ═══════════════════════════════════════════════════════════
if (location.hash) idInput.value = location.hash.slice(1)

loadSessions()
loadPrompts()
loadTools()

if (idInput.value) loadContext(idInput.value)

function loadById(id) {
  if (!id) return
  if (currentTab === 'planning') loadPlanningLogs(id)
  else loadContext(id)
}
btnLoad.addEventListener('click', () => loadById(idInput.value.trim()))
idInput.addEventListener('keydown', e => { if (e.key === 'Enter') loadById(idInput.value.trim()) })
btnRefresh.addEventListener('click', () => {
  if (currentTab === 'planning') { if (currentPlanSession) loadPlanningLogs(currentPlanSession) }
  else { if (currentSession) loadContext(currentSession) }
})
btnAuto.addEventListener('click', toggleAuto)
btnReloadPt.addEventListener('click', () => { loadPrompts(); loadTools() })
toolFilterEl.addEventListener('input', () => renderTools(allTools))

// ═══════════════════════════════════════════════════════════
// Context tab
// ═══════════════════════════════════════════════════════════
async function loadSessions() {
  try {
    const sessions = await apiFetch('/sessions')
    renderSessionList(sessions)
  } catch {
    sessionListEl.innerHTML = '<div style="padding:10px;color:#555;font-size:11px;">failed to load</div>'
  }
}

function renderSessionList(sessions) {
  sessionListEl.innerHTML = ''
  if (!sessions.length) {
    sessionListEl.innerHTML = '<div style="padding:12px;color:#555;font-size:11px;">no sessions</div>'
    return
  }
  for (const s of sessions) {
    const el = document.createElement('div')
    el.className = 'list-item' + (s.session_id === currentSession ? ' selected' : '')
    el.dataset.id = s.session_id
    el.innerHTML = `
      <div class="li-name" title="${s.session_id}">${esc(s.name || s.preview || s.session_id.slice(0,14)+'…')}</div>
      <div class="li-meta">
        <span class="li-profile">${esc(s.profile_id)}</span>
        <span>${s.message_count} msg</span>
        ${s.pinned ? '<span class="li-pin">★</span>' : ''}
      </div>`
    el.addEventListener('click', () => loadContext(s.session_id))
    sessionListEl.appendChild(el)
  }
}

async function loadContext(id) {
  if (!id) return
  currentSession = id
  idInput.value = id
  location.hash = id
  statsEl.textContent = 'loading…'
  msgsEl.innerHTML = ''
  errEl.hidden = true
  phEl.hidden = true

  document.querySelectorAll('#session-list .list-item').forEach(el =>
    el.classList.toggle('selected', el.dataset.id === id)
  )

  try {
    const data = await apiFetch(`/sessions/${encodeURIComponent(id)}/context`)
    renderContext(data)
  } catch (e) {
    errEl.textContent = e.message
    errEl.hidden = false
    statsEl.textContent = ''
  }
}

function renderContext(data) {
  const estTokens = Math.round(data.total_chars / 4)
  statsEl.innerHTML =
    `<b>${data.profile_id}</b> · ` +
    `<b>${data.message_count}</b> msgs · ` +
    `<b>${data.total_chars.toLocaleString()}</b> chars · ` +
    `~<b>${estTokens.toLocaleString()}</b> tok`

  msgsEl.innerHTML = ''
  data.context.forEach((msg, idx) => msgsEl.appendChild(renderMessage(msg, idx)))
}

function renderMessage(msg, idx) {
  const el = document.createElement('div')
  el.className = 'msg'

  // Left: role column
  const roleCol = document.createElement('div')
  roleCol.className = 'msg-role'

  const badge = document.createElement('span')
  badge.className = `role-badge role-${msg.role}`
  badge.textContent = msg.role
  roleCol.appendChild(badge)

  const tags = []
  if (msg.is_summary)         tags.push(['summary', 'tag-summary'])
  if (msg.is_plan)            tags.push(['plan',    'tag-plan'])
  if (msg.thinking)           tags.push(['think',   'tag-think'])
  if (msg.images?.length)     tags.push([msg.images.length + ' img', 'tag-images'])
  if (msg.tool_calls?.length) tags.push([msg.tool_calls.length + ' call' + (msg.tool_calls.length > 1 ? 's' : ''), 'tag-tools'])

  for (const [lbl, cls] of tags) {
    const t = document.createElement('span')
    t.className = `tag ${cls}`
    t.textContent = lbl
    roleCol.appendChild(t)
  }
  const idxEl = document.createElement('span')
  idxEl.className = 'msg-idx'
  idxEl.textContent = `#${idx}`
  roleCol.appendChild(idxEl)

  // Right: body
  const body = document.createElement('div')
  body.className = 'msg-body'

  if (msg.thinking) {
    body.appendChild(sectionLabel('thinking'))
    const pre = document.createElement('pre')
    pre.className = 'content thinking-text'
    pre.textContent = msg.thinking
    body.appendChild(pre)
  }

  if (msg.content) {
    if (msg.thinking) body.appendChild(sectionLabel('response'))
    const pre = document.createElement('pre')
    pre.className = 'content' + (msg.role === 'tool' ? ' dim' : '')
    pre.textContent = msg.content
    body.appendChild(pre)
  }

  if (msg.tool_calls?.length) {
    body.appendChild(sectionLabel('tool calls'))
    for (const tc of msg.tool_calls) {
      const block = document.createElement('div')
      block.className = 'tool-call-block'
      const name = mk('div', 'tool-call-name', tc.name || tc.function?.name || '?')
      const args = mk('pre', 'tool-call-args', JSON.stringify(tc.arguments ?? tc.function?.arguments ?? {}, null, 2))
      block.appendChild(name); block.appendChild(args)
      body.appendChild(block)
    }
  }

  if (msg.tool_call_id) {
    body.appendChild(mk('div', 'tool-id',
      `↳ call_id: ${msg.tool_call_id}${msg.name ? '  tool: ' + msg.name : ''}`))
  }

  if (msg.images?.length) {
    body.appendChild(sectionLabel('images'))
    const strip = document.createElement('div')
    strip.className = 'img-strip'
    for (const b64 of msg.images) {
      const img = document.createElement('img')
      img.className = 'img-thumb'
      img.src = b64.startsWith('data:') ? b64 : `data:image/jpeg;base64,${b64}`
      img.addEventListener('click', () => window.open(img.src))
      strip.appendChild(img)
    }
    body.appendChild(strip)
  }

  const chars = (msg.content || '').length + (msg.thinking || '').length
  if (chars > 0) body.appendChild(mk('div', 'char-count',
    `${chars.toLocaleString()} chars · ~${Math.round(chars/4).toLocaleString()} tok`))

  el.appendChild(roleCol)
  el.appendChild(body)
  return el
}

function toggleAuto() {
  if (autoTimer) {
    clearInterval(autoTimer)
    autoTimer = null
    btnAuto.classList.remove('active')
    btnAuto.textContent = 'Auto'
  } else {
    btnAuto.classList.add('active')
    btnAuto.textContent = 'Auto ●'
    autoTimer = setInterval(() => {
      if (currentSession) loadContext(currentSession)
      loadSessions()
    }, 3000)
  }
}

// ═══════════════════════════════════════════════════════════
// Planning tab
// ═══════════════════════════════════════════════════════════
const planningSessionListEl = document.getElementById('planning-session-list')
const planningContent       = document.getElementById('planning-content')
const planningPh            = document.getElementById('planning-placeholder')

async function loadPlanningSessions() {
  try {
    const sessions = await apiFetch('/sessions')
    renderPlanningSessionList(sessions)
  } catch {
    planningSessionListEl.innerHTML = '<div style="padding:10px;color:#555;font-size:11px;">failed to load</div>'
  }
}

function renderPlanningSessionList(sessions) {
  planningSessionListEl.innerHTML = ''
  if (!sessions.length) {
    planningSessionListEl.innerHTML = '<div style="padding:12px;color:#555;font-size:11px;">no sessions</div>'
    return
  }
  for (const s of sessions) {
    const el = document.createElement('div')
    el.className = 'list-item' + (s.session_id === currentPlanSession ? ' selected' : '')
    el.dataset.id = s.session_id
    el.innerHTML = `
      <div class="li-name" title="${s.session_id}">${esc(s.name || s.preview || s.session_id.slice(0,14)+'…')}</div>
      <div class="li-meta">
        <span class="li-profile">${esc(s.profile_id)}</span>
        <span>${s.message_count} msg</span>
      </div>`
    el.addEventListener('click', () => loadPlanningLogs(s.session_id))
    planningSessionListEl.appendChild(el)
  }
}

async function loadPlanningLogs(id) {
  if (!id) return
  currentPlanSession = id
  idInput.value = id
  location.hash = id
  document.querySelectorAll('#planning-session-list .list-item').forEach(el =>
    el.classList.toggle('selected', el.dataset.id === id))
  planningContent.innerHTML = ''
  planningPh.classList.add('hidden')

  try {
    const data = await apiFetch(`/sessions/${encodeURIComponent(id)}/planning`)
    renderPlanningLogs(data.logs)
  } catch (e) {
    planningContent.innerHTML = `<div style="padding:20px;color:#f48771;">${esc(e.message)}</div>`
  }
}

function renderPlanningLogs(logs) {
  planningContent.innerHTML = ''
  if (!logs.length) {
    planningPh.textContent = 'No planning logs for this session.'
    planningPh.classList.remove('hidden')
    return
  }

  const phaseLabels = { '1': 'Phase 1 — Analysis', '2': 'Phase 2 — Advisor review', '3': 'Phase 3 — Execution plan' }
  const phaseClass  = { '1': 'phase-1', '2': 'phase-2', '3': 'phase-3' }

  // Show newest first
  for (const log of [...logs].reverse()) {
    const run = document.createElement('div')
    run.className = 'plan-run'

    const ts = new Date(log.timestamp).toLocaleString()
    const resultCls = log.result === 'plan' ? 'result-plan' : log.result === 'direct' ? 'result-direct' : 'result-other'

    const hdr = document.createElement('div')
    hdr.className = 'plan-run-header'
    hdr.innerHTML = `
      <span class="ps-chevron plan-run-chevron">▶</span>
      <span class="plan-run-ts">${esc(ts)}</span>
      <span class="plan-run-result ${resultCls}">${esc(log.result)}</span>`

    const body = document.createElement('div')
    body.className = 'plan-run-body hidden'

    const phases = log.phases || {}
    for (const [num, phase] of Object.entries(phases)) {
      const block = document.createElement('div')
      block.className = 'phase-block'

      // Phase 2 stores advisor results as {Critic: {...}, Pragmatist: {...}, Detailer: {...}}
      const isAdvisorPhase = num === '2' && phase && typeof phase === 'object' && !('output' in phase)

      const totalTok = isAdvisorPhase
        ? Object.values(phase).reduce((s, a) => s + (a.prompt_tokens||0) + (a.completion_tokens||0), 0)
        : (phase.prompt_tokens || 0) + (phase.completion_tokens || 0)

      const phHdr = document.createElement('div')
      phHdr.className = 'phase-header'
      phHdr.innerHTML = `
        <span class="ps-chevron phase-chevron open">▶</span>
        <span class="phase-label ${phaseClass[num] || ''}">${esc(phaseLabels[num] || 'Phase ' + num)}</span>
        <span class="phase-tokens">${totalTok.toLocaleString()} tok</span>`

      const phBody = document.createElement('div')
      phBody.className = 'phase-body'

      if (isAdvisorPhase) {
        // Render each advisor as a sub-block
        for (const [advisor, data] of Object.entries(phase)) {
          const advBlock = document.createElement('div')
          advBlock.className = 'advisor-block'
          const tok = (data.prompt_tokens||0) + (data.completion_tokens||0)
          advBlock.innerHTML = `<div class="advisor-header"><strong>${esc(advisor)}</strong> <span class="phase-tokens">${tok.toLocaleString()} tok</span></div>`
          const pre = document.createElement('pre')
          pre.textContent = data.output || '(empty)'
          advBlock.appendChild(pre)
          phBody.appendChild(advBlock)
        }
      } else {
        const pre = document.createElement('pre')
        pre.textContent = phase.output || '(empty)'
        phBody.appendChild(pre)
      }

      const phChevron = phHdr.querySelector('.phase-chevron')
      phHdr.addEventListener('click', () => {
        const hidden = phBody.classList.toggle('hidden')
        phChevron.classList.toggle('open', !hidden)
      })

      block.appendChild(phHdr)
      block.appendChild(phBody)
      body.appendChild(block)
    }

    const chevron = hdr.querySelector('.plan-run-chevron')
    hdr.addEventListener('click', () => {
      const isHidden = body.classList.toggle('hidden')
      chevron.classList.toggle('open', !isHidden)
    })

    run.appendChild(hdr)
    run.appendChild(body)
    planningContent.appendChild(run)
  }
}

// ═══════════════════════════════════════════════════════════
// Prompts tab
// ═══════════════════════════════════════════════════════════
async function loadPrompts() {
  try {
    allPrompts = await apiFetch('/agents/prompts')
    renderProfileList(allPrompts)
    if (currentProfile) {
      const p = allPrompts.find(p => p.profile_id === currentProfile)
      if (p) renderPrompt(p)
    }
  } catch (e) {
    profileListEl.innerHTML = `<div style="padding:10px;color:#555;font-size:11px;">${e.message}</div>`
  }
}

function renderProfileList(prompts) {
  profileListEl.innerHTML = ''
  for (const p of prompts) {
    const el = document.createElement('div')
    el.className = 'list-item' + (p.profile_id === currentProfile ? ' selected' : '')
    el.dataset.id = p.profile_id
    const chars = p.total_chars
    const tok = Math.round(chars / 4)
    el.innerHTML = `
      <div class="li-name">${esc(p.profile_name)}</div>
      <div class="li-meta">
        <span class="li-profile">${esc(p.profile_id)}</span>
        <span>~${tok.toLocaleString()} tok</span>
      </div>`
    el.addEventListener('click', () => {
      currentProfile = p.profile_id
      document.querySelectorAll('#profile-list .list-item').forEach(e =>
        e.classList.toggle('selected', e.dataset.id === p.profile_id))
      renderPrompt(p)
    })
    profileListEl.appendChild(el)
  }
}

function renderPrompt(p) {
  promptsPh.classList.add('hidden')
  promptsContent.innerHTML = ''

  const card = document.createElement('div')
  card.className = 'prompt-card'

  // Header row
  const hdr = document.createElement('div')
  hdr.className = 'profile-header'
  hdr.innerHTML = `
    <h2>${esc(p.profile_name)}</h2>
    <span class="ph-model">${esc(p.model)}</span>
    <span class="ph-chars">${p.total_chars.toLocaleString()} chars · ~${Math.round(p.total_chars/4).toLocaleString()} tok</span>
    <span class="ph-tools-badge" title="Click to show tools">⚙ ${p.enabled_tools.length} tools</span>`
  card.appendChild(hdr)

  // Tools pills (togglable)
  const toolsWrap = document.createElement('div')
  toolsWrap.className = 'tools-inline hidden'
  for (const t of p.enabled_tools) {
    const pill = document.createElement('span')
    pill.className = 'tool-pill'
    pill.textContent = t
    toolsWrap.appendChild(pill)
  }
  card.appendChild(toolsWrap)

  hdr.querySelector('.ph-tools-badge').addEventListener('click', () =>
    toolsWrap.classList.toggle('hidden'))

  // Sections
  const labelClasses = { persona: 'lbl-persona', profile: 'lbl-profile', 'profiles block': 'lbl-profiles' }

  for (const section of p.sections) {
    const sec = document.createElement('div')
    sec.className = 'prompt-section'

    const chars = section.content.length
    const secHdr = document.createElement('div')
    secHdr.className = 'prompt-section-header'
    secHdr.innerHTML = `
      <span class="ps-chevron open">▶</span>
      <span class="ps-label ${labelClasses[section.label] || ''}">${esc(section.label)}</span>
      <span class="ps-chars">${chars.toLocaleString()} chars · ~${Math.round(chars/4).toLocaleString()} tok</span>`

    const body = document.createElement('div')
    body.className = 'prompt-section-body'
    const pre = document.createElement('pre')
    pre.textContent = section.content
    body.appendChild(pre)

    const chevron = secHdr.querySelector('.ps-chevron')
    secHdr.addEventListener('click', () => {
      const collapsed = body.classList.toggle('hidden')
      chevron.classList.toggle('open', !collapsed)
    })

    sec.appendChild(secHdr)
    sec.appendChild(body)
    card.appendChild(sec)
  }

  promptsContent.appendChild(card)
}

// ═══════════════════════════════════════════════════════════
// Tools tab
// ═══════════════════════════════════════════════════════════
async function loadTools() {
  try {
    allTools = await apiFetch('/agents/tools')
    renderTools(allTools)
  } catch (e) {
    toolsContent.innerHTML = `<div style="padding:20px;color:#555;">${e.message}</div>`
  }
}

function renderTools(tools) {
  const q = toolFilterEl.value.trim().toLowerCase()
  const filtered = q ? tools.filter(t =>
    t.name.includes(q) || t.description.toLowerCase().includes(q)
  ) : tools

  toolsContent.innerHTML = ''

  for (const tool of filtered) {
    const card = document.createElement('div')
    card.className = 'tool-card'

    const hdr = document.createElement('div')
    hdr.className = 'tool-card-header'
    // Heuristic: tools with underscores that aren't snake-case-class are user tools.
    // For now just show the name; no reliable builtin flag from API yet.
    hdr.innerHTML = `
      <span class="ps-chevron">▶</span>
      <span class="tool-name">${esc(tool.name)}</span>`

    const body = document.createElement('div')
    body.className = 'tool-card-body hidden'

    const desc = document.createElement('div')
    desc.className = 'tool-desc'
    desc.textContent = tool.description
    body.appendChild(desc)

    const props = tool.parameters?.properties || {}
    const required = tool.parameters?.required || []
    const propKeys = Object.keys(props)

    if (propKeys.length) {
      const plbl = document.createElement('div')
      plbl.className = 'params-label'
      plbl.textContent = 'parameters'
      body.appendChild(plbl)

      for (const key of propKeys) {
        const prop = props[key]
        const row = document.createElement('div')
        row.className = 'param-row'
        const isReq = required.includes(key)
        const typeStr = prop.enum ? `enum` : (prop.type || '?')
        const descStr = prop.description || (prop.enum ? prop.enum.join(' | ') : '')
        row.innerHTML = `
          <span class="param-name">${esc(key)}${isReq ? '<span class="param-req">*</span>' : ''}</span>
          <span class="param-type">${esc(typeStr)}</span>
          <span class="param-desc">${esc(descStr)}</span>`
        body.appendChild(row)
      }
    } else if (tool.parameters && Object.keys(tool.parameters).length === 0) {
      const none = document.createElement('div')
      none.className = 'params-label'
      none.textContent = 'no parameters'
      body.appendChild(none)
    }

    const chevron = hdr.querySelector('.ps-chevron')
    hdr.addEventListener('click', () => {
      const hidden = body.classList.toggle('hidden')
      chevron.classList.toggle('open', !hidden)
    })

    card.appendChild(hdr)
    card.appendChild(body)
    toolsContent.appendChild(card)
  }
}

// ═══════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════
async function apiFetch(path) {
  const res = await fetch(path)
  if (!res.ok) {
    const j = await res.json().catch(() => ({}))
    throw new Error(j.detail || `HTTP ${res.status}`)
  }
  return res.json()
}

function esc(s) {
  return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
}

function mk(tag, cls, text) {
  const el = document.createElement(tag)
  el.className = cls
  el.textContent = text
  return el
}

function sectionLabel(text) {
  const el = document.createElement('div')
  el.className = 'section-label'
  el.textContent = text
  return el
}
</script>
</body>
</html>