<!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-mcp { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #1e3a3a; color: var(--c-system); }
.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>
<button class="tab" data-tab="mcp">MCP</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>
<!-- ── MCP sidebar ── -->
<aside id="mcp-aside" class="hidden">
<div class="aside-title">MCP Servers</div>
<div class="aside-scroll" id="mcp-server-list"></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>
<!-- MCP view -->
<div id="view-mcp" class="hidden">
<div id="mcp-placeholder" style="text-align:center;color:#555;padding:80px 0;font-size:12px;">Select an MCP server.</div>
<div id="mcp-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 = []
let allMcpServers = []
let currentMcpServer = null
// ═══════════════════════════════════════════════════════════
// 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')
const mcpServerListEl = document.getElementById('mcp-server-list')
const mcpContent = document.getElementById('mcp-content')
const mcpPh = document.getElementById('mcp-placeholder')
// ═══════════════════════════════════════════════════════════
// 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('mcp-aside', tab !== 'mcp')
setHidden('view-context', tab !== 'context')
setHidden('view-planning', tab !== 'planning')
setHidden('view-prompts', tab !== 'prompts')
setHidden('view-tools', tab !== 'tools')
setHidden('view-mcp', tab !== 'mcp')
setHidden('ctx-controls', tab !== 'context' && tab !== 'planning')
setHidden('pt-controls', tab === 'context' || tab === 'planning')
if (tab === 'planning') loadPlanningSessions()
if (tab === 'mcp') loadMcpServers()
}
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()
loadMcpServers()
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) — built-in + resolved MCP
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)
}
for (const t of (p.resolved_mcp_tools || [])) {
const pill = document.createElement('span')
pill.className = 'tool-pill'
pill.style.cssText = 'background:#1e3a3a;color:var(--c-system);border-color:#2a5a5a;'
pill.textContent = t
toolsWrap.appendChild(pill)
}
card.appendChild(toolsWrap)
hdr.querySelector('.ph-tools-badge').addEventListener('click', () =>
toolsWrap.classList.toggle('hidden'))
// MCP servers
if (p.mcp_servers && Object.keys(p.mcp_servers).length) {
const mcpWrap = document.createElement('div')
mcpWrap.className = 'tools-inline'
const label = document.createElement('span')
label.className = 'tool-pill'
label.style.cssText = 'background:transparent;border:none;color:var(--text3);padding-left:0;cursor:default;'
label.textContent = 'MCP servers:'
mcpWrap.appendChild(label)
for (const [server, groups] of Object.entries(p.mcp_servers)) {
const pill = document.createElement('span')
pill.className = 'tool-pill'
pill.style.cssText = 'background:#1e3a3a;color:var(--c-system);border-color:#2a5a5a;'
pill.textContent = `${esc(server)} [${groups.join(', ')}]`
mcpWrap.appendChild(pill)
}
card.appendChild(mcpWrap)
}
// 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 isMcp = tool.name.startsWith('mcp_')
const hdr = document.createElement('div')
hdr.className = 'tool-card-header'
hdr.innerHTML = `
<span class="ps-chevron">▶</span>
<span class="tool-name">${esc(tool.name)}</span>
${isMcp ? '<span class="tool-mcp">mcp</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)
}
}
// ═══════════════════════════════════════════════════════════
// MCP tab
// ═══════════════════════════════════════════════════════════
async function loadMcpServers() {
try {
allMcpServers = await apiFetch('/agents/mcp_servers')
renderMcpServerList(allMcpServers)
if (currentMcpServer) {
const s = allMcpServers.find(s => s.name === currentMcpServer)
if (s) renderMcpServer(s)
}
} catch (e) {
mcpServerListEl.innerHTML = `<div style="padding:10px;color:#555;font-size:11px;">${e.message}</div>`
}
}
function renderMcpServerList(servers) {
mcpServerListEl.innerHTML = ''
if (!servers.length) {
mcpServerListEl.innerHTML = '<div style="padding:12px;color:#555;font-size:11px;">no MCP servers</div>'
return
}
for (const s of servers) {
const el = document.createElement('div')
el.className = 'list-item' + (s.name === currentMcpServer ? ' selected' : '')
el.dataset.name = s.name
const statusColor = s.connected ? 'var(--accent)' : '#f48771'
const statusText = s.connected ? '●' : '○'
el.innerHTML = `
<div class="li-name">${esc(s.name)}</div>
<div class="li-meta">
<span style="color:${statusColor}">${statusText} ${s.connected ? 'connected' : 'offline'}</span>
<span>${esc(s.transport)}</span>
</div>`
el.addEventListener('click', () => {
currentMcpServer = s.name
document.querySelectorAll('#mcp-server-list .list-item').forEach(e =>
e.classList.toggle('selected', e.dataset.name === s.name))
renderMcpServer(s)
})
mcpServerListEl.appendChild(el)
}
}
function renderMcpServer(s) {
mcpPh.classList.add('hidden')
mcpContent.innerHTML = ''
const card = document.createElement('div')
card.className = 'prompt-card'
// Header
const hdr = document.createElement('div')
hdr.className = 'profile-header'
const statusColor = s.connected ? 'var(--accent)' : '#f48771'
hdr.innerHTML = `
<h2>${esc(s.name)}</h2>
<span class="ph-model" style="color:${statusColor}">${s.connected ? '● connected' : '○ offline'}</span>
<span class="ph-chars">${esc(s.transport)}${s.url ? ' · ' + esc(s.url) : s.command ? ' · ' + esc(s.command) : ''}</span>`
card.appendChild(hdr)
// Groups
if (s.groups && Object.keys(s.groups).length) {
const groupsWrap = document.createElement('div')
groupsWrap.className = 'tools-inline'
const label = document.createElement('span')
label.className = 'tool-pill'
label.style.cssText = 'background:transparent;border:none;color:var(--text3);padding-left:0;cursor:default;'
label.textContent = 'Groups:'
groupsWrap.appendChild(label)
for (const [group, tools] of Object.entries(s.groups)) {
const pill = document.createElement('span')
pill.className = 'tool-pill'
pill.title = tools.join('\n')
pill.textContent = `${esc(group)} (${tools.length})`
groupsWrap.appendChild(pill)
}
card.appendChild(groupsWrap)
}
// Profile mappings
if (s.profiles && s.profiles.length) {
const profWrap = document.createElement('div')
profWrap.className = 'tools-inline'
const label = document.createElement('span')
label.className = 'tool-pill'
label.style.cssText = 'background:transparent;border:none;color:var(--text3);padding-left:0;cursor:default;'
label.textContent = 'Used by:'
profWrap.appendChild(label)
for (const pr of s.profiles) {
const pill = document.createElement('span')
pill.className = 'tool-pill'
pill.style.cssText = 'background:#2d1e3a;color:var(--c-assistant);border-color:#3d2e4a;'
pill.textContent = `${esc(pr.profile_id)} [${pr.groups.join(', ')}]`
profWrap.appendChild(pill)
}
card.appendChild(profWrap)
}
// Instructions
if (s.instructions) {
const sec = document.createElement('div')
sec.className = 'prompt-section'
const secHdr = document.createElement('div')
secHdr.className = 'prompt-section-header'
secHdr.innerHTML = `
<span class="ps-chevron open">▶</span>
<span class="ps-label lbl-persona">Instructions</span>
<span class="ps-chars">${s.instructions.length.toLocaleString()} chars</span>`
const body = document.createElement('div')
body.className = 'prompt-section-body'
const pre = document.createElement('pre')
pre.textContent = s.instructions
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)
}
// Tools exposed by server
if (s.tools && s.tools.length) {
const sec = document.createElement('div')
sec.className = 'prompt-section'
const secHdr = document.createElement('div')
secHdr.className = 'prompt-section-header'
secHdr.innerHTML = `
<span class="ps-chevron open">▶</span>
<span class="ps-label lbl-profile">Tools (${s.tools.length})</span>`
const body = document.createElement('div')
body.className = 'prompt-section-body'
for (const t of s.tools) {
const row = document.createElement('div')
row.style.cssText = 'padding:4px 0;border-top:1px solid var(--border);font-size:11px;'
row.innerHTML = `
<span style="color:var(--c-tool);font-weight:600;">${esc(t.name)}</span>
<span style="color:var(--text2);margin-left:8px;">${esc(t.description)}</span>`
body.appendChild(row)
}
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)
}
mcpContent.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,'&').replace(/</g,'<').replace(/>/g,'>')
}
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>