<!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;
--border: #252525;
--border2: #2e2e2e;
--text: #d4d4d4;
--text2: #888;
--text3: #555;
--accent: #4ec9b0;
--c-system: #4ec9b0;
--c-user: #569cd6;
--c-assistant: #c586c0;
--c-tool: #ce9178;
--bg-system: #1e3a2f;
--bg-user: #1e2d40;
--bg-assistant: #2d1e3a;
--bg-tool: #3a2d1e;
}
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: 8px 14px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.logo { font-size: 11px; color: var(--text3); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; }
.logo b { color: var(--accent); }
.spacer { flex: 1; }
select, button, input {
font-family: inherit;
font-size: 12px;
background: var(--bg3);
border: 1px solid var(--border2);
color: var(--text);
border-radius: 4px;
outline: none;
}
select { padding: 4px 8px; cursor: pointer; max-width: 320px; }
select:focus { border-color: #555; }
input { padding: 4px 8px; width: 280px; }
input:focus { border-color: #555; }
button { padding: 4px 10px; cursor: pointer; }
button:hover { background: #2a2a2a; 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: 230px;
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;
}
#session-list {
flex: 1;
overflow-y: auto;
}
.session-item {
padding: 7px 10px;
cursor: pointer;
border-bottom: 1px solid var(--border);
transition: background .1s;
}
.session-item:hover { background: var(--bg3); }
.session-item.selected { background: #1a1a2a; border-left: 2px solid var(--c-user); padding-left: 8px; }
.si-name {
font-size: 11px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.si-meta {
font-size: 10px;
color: var(--text3);
margin-top: 2px;
display: flex;
gap: 6px;
}
.si-profile { color: var(--text2); }
.si-pin { color: var(--accent); }
/* ── Main content ── */
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;
}
/* ── Message card ── */
.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: var(--bg-system); color: var(--c-system); }
.role-user { background: var(--bg-user); color: var(--c-user); }
.role-assistant { background: var(--bg-assistant); color: var(--c-assistant); }
.role-tool { background: var(--bg-tool); 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); }
/* ── Message body ── */
.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: #222;
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; }
.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; }
.char-count { font-size: 10px; color: var(--text3); margin-top: 6px; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #444; }
</style>
</head>
<body>
<header>
<div class="logo"><b>navi</b> debug</div>
<input id="id-input" type="text" placeholder="session id…" spellcheck="false">
<button id="btn-load">Load</button>
<button id="btn-refresh" title="Reload context">↺</button>
<button id="btn-auto" title="Auto-refresh every 3s">Auto</button>
<div class="spacer"></div>
<div id="stats"></div>
</header>
<div class="layout">
<aside>
<div class="aside-title">Sessions</div>
<div id="session-list"><div style="padding:12px;color:#555;font-size:11px;">loading…</div></div>
</aside>
<main id="main">
<div id="placeholder">Select a session or enter an ID above.</div>
<div id="error-msg" hidden></div>
<div id="messages"></div>
</main>
</div>
<script>
// ── State ──────────────────────────────────────────────────────────────────
let currentId = null
let autoTimer = null
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 statsEl = document.getElementById('stats')
const msgsEl = document.getElementById('messages')
const errEl = document.getElementById('error-msg')
const phEl = document.getElementById('placeholder')
const listEl = document.getElementById('session-list')
// ── Init ───────────────────────────────────────────────────────────────────
if (location.hash) idInput.value = location.hash.slice(1)
loadSessions()
if (idInput.value) loadContext(idInput.value)
btnLoad.addEventListener('click', () => loadContext(idInput.value.trim()))
idInput.addEventListener('keydown', e => { if (e.key === 'Enter') loadContext(idInput.value.trim()) })
btnRefresh.addEventListener('click', () => { if (currentId) loadContext(currentId) })
btnAuto.addEventListener('click', toggleAuto)
// ── Session list ───────────────────────────────────────────────────────────
async function loadSessions() {
try {
const res = await fetch('/sessions')
const sessions = await res.json()
renderSessionList(sessions)
} catch {
listEl.innerHTML = '<div style="padding:10px;color:#555;font-size:11px;">failed to load</div>'
}
}
function renderSessionList(sessions) {
listEl.innerHTML = ''
if (!sessions.length) {
listEl.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 = 'session-item' + (s.session_id === currentId ? ' selected' : '')
el.dataset.id = s.session_id
const name = document.createElement('div')
name.className = 'si-name'
name.textContent = s.name || s.preview || s.session_id.slice(0, 12) + '…'
name.title = s.session_id
const meta = document.createElement('div')
meta.className = 'si-meta'
meta.innerHTML = `<span class="si-profile">${s.profile_id}</span><span>${s.message_count} msg</span>${s.pinned ? '<span class="si-pin">★</span>' : ''}`
el.appendChild(name)
el.appendChild(meta)
el.addEventListener('click', () => loadContext(s.session_id))
listEl.appendChild(el)
}
}
// ── Context load ───────────────────────────────────────────────────────────
async function loadContext(id) {
if (!id) return
currentId = id
idInput.value = id
location.hash = id
statsEl.textContent = 'loading…'
msgsEl.innerHTML = ''
errEl.hidden = true
phEl.hidden = true
// Highlight selected item
document.querySelectorAll('.session-item').forEach(el => {
el.classList.toggle('selected', el.dataset.id === id)
})
try {
const res = await fetch(`/sessions/${encodeURIComponent(id)}/context`)
if (!res.ok) {
const j = await res.json().catch(() => ({}))
throw new Error(j.detail || `HTTP ${res.status}`)
}
const data = await res.json()
renderContext(data)
} catch (e) {
errEl.textContent = e.message
errEl.hidden = false
statsEl.textContent = ''
}
}
// ── Render ─────────────────────────────────────────────────────────────────
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 [label, cls] of tags) {
const t = document.createElement('span')
t.className = `tag ${cls}`
t.textContent = label
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'
// Thinking block
if (msg.thinking) {
const lbl = label('thinking')
body.appendChild(lbl)
const pre = document.createElement('pre')
pre.className = 'content thinking-text'
pre.textContent = msg.thinking
body.appendChild(pre)
}
// Main content
if (msg.content) {
if (msg.thinking) body.appendChild(label('response'))
const pre = document.createElement('pre')
pre.className = 'content' + (msg.role === 'tool' ? ' dim' : '')
pre.textContent = msg.content
body.appendChild(pre)
}
// Tool calls
if (msg.tool_calls?.length) {
body.appendChild(label('tool calls'))
for (const tc of msg.tool_calls) {
const block = document.createElement('div')
block.className = 'tool-call-block'
const name = document.createElement('div')
name.className = 'tool-call-name'
name.textContent = tc.name || tc.function?.name || '?'
const args = document.createElement('pre')
args.className = 'tool-call-args'
const rawArgs = tc.arguments ?? tc.function?.arguments ?? {}
args.textContent = JSON.stringify(rawArgs, null, 2)
block.appendChild(name)
block.appendChild(args)
body.appendChild(block)
}
}
// Tool call ID (tool result messages)
if (msg.tool_call_id) {
const tid = document.createElement('div')
tid.className = 'tool-id'
tid.textContent = `↳ call_id: ${msg.tool_call_id}${msg.name ? ' tool: ' + msg.name : ''}`
body.appendChild(tid)
}
// Images
if (msg.images?.length) {
body.appendChild(label('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)
}
// Char count
const chars = (msg.content || '').length + (msg.thinking || '').length
if (chars > 0) {
const cc = document.createElement('div')
cc.className = 'char-count'
cc.textContent = `${chars.toLocaleString()} chars · ~${Math.round(chars / 4).toLocaleString()} tok`
body.appendChild(cc)
}
el.appendChild(roleCol)
el.appendChild(body)
return el
}
function label(text) {
const el = document.createElement('div')
el.className = 'section-label'
el.textContent = text
return el
}
// ── Auto-refresh ───────────────────────────────────────────────────────────
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 (currentId) loadContext(currentId)
loadSessions()
}, 3000)
}
}
</script>
</body>
</html>