200 lines
9.1 KiB
JavaScript
200 lines
9.1 KiB
JavaScript
/**
|
|
* Toolbar — top bar with session info and actions
|
|
*/
|
|
|
|
import { state, deleteSession, toggleTheme } from '../app.js';
|
|
import { api } from '../api.js';
|
|
|
|
let sessionIdEl;
|
|
let statusBadge;
|
|
|
|
export function initToolbar() {
|
|
const toolbar = document.getElementById('toolbar');
|
|
|
|
toolbar.innerHTML = `
|
|
<span class="toolbar-brand">Agentic Microservice</span>
|
|
<span class="toolbar-separator"></span>
|
|
<span class="toolbar-session-id" id="toolbar-session-id" title="Click to copy">No session</span>
|
|
<span class="badge idle" id="toolbar-status" style="display:none"></span>
|
|
<span class="toolbar-spacer"></span>
|
|
<div class="toolbar-actions">
|
|
<button class="btn btn-sm" id="btn-refresh" title="Refresh state">↻ Refresh</button>
|
|
<button class="btn btn-sm" id="btn-context-debug" title="View context debug">🔎 Context</button>
|
|
<button class="btn btn-sm" id="btn-raw-state" title="View raw JSON">{ } Raw</button>
|
|
<button class="btn btn-sm" id="btn-theme" title="Toggle theme">◑</button>
|
|
<button class="btn btn-sm btn-danger" id="btn-delete-session" title="Delete session">🗑</button>
|
|
</div>
|
|
`;
|
|
|
|
sessionIdEl = document.getElementById('toolbar-session-id');
|
|
statusBadge = document.getElementById('toolbar-status');
|
|
|
|
// Copy session ID
|
|
sessionIdEl.addEventListener('click', () => {
|
|
if (state.activeSessionId) {
|
|
navigator.clipboard.writeText(state.activeSessionId);
|
|
const orig = sessionIdEl.textContent;
|
|
sessionIdEl.textContent = 'Copied!';
|
|
setTimeout(() => { sessionIdEl.textContent = orig; }, 1000);
|
|
}
|
|
});
|
|
|
|
// Refresh
|
|
document.getElementById('btn-refresh').addEventListener('click', async () => {
|
|
if (!state.activeSessionId) return;
|
|
try {
|
|
const session = await api.getSession(state.activeSessionId);
|
|
state.sessionState = session;
|
|
updateToolbar(session);
|
|
// Dispatch event so inspector updates too
|
|
const { updateInspector } = await import('./session-inspector.js');
|
|
updateInspector(session);
|
|
} catch (e) {
|
|
alert(`Refresh failed: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
// Context debug
|
|
document.getElementById('btn-context-debug').addEventListener('click', async () => {
|
|
if (!state.activeSessionId) return;
|
|
try {
|
|
const res = await fetch(`/api/v1/sessions/${state.activeSessionId}/context-debug`);
|
|
const data = await res.json();
|
|
const win = window.open('', '_blank', 'width=900,height=700');
|
|
win.document.title = 'Context Debug';
|
|
win.document.write(renderContextDebugHTML(data));
|
|
} catch (e) {
|
|
alert(`Failed: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
// Raw state
|
|
document.getElementById('btn-raw-state').addEventListener('click', async () => {
|
|
if (!state.activeSessionId) return;
|
|
try {
|
|
const session = await api.getSession(state.activeSessionId);
|
|
const win = window.open('', '_blank', 'width=600,height=500');
|
|
win.document.write(`<pre style="font-size:12px;padding:16px;background:#0d1117;color:#e6edf3;font-family:monospace">${JSON.stringify(session, null, 2)}</pre>`);
|
|
} catch (e) {
|
|
alert(`Failed: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
// Theme
|
|
document.getElementById('btn-theme').addEventListener('click', toggleTheme);
|
|
|
|
// Delete
|
|
document.getElementById('btn-delete-session').addEventListener('click', () => {
|
|
if (!state.activeSessionId) return;
|
|
if (confirm('Delete this session permanently?')) {
|
|
deleteSession(state.activeSessionId);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function updateToolbar(session) {
|
|
if (!session) {
|
|
sessionIdEl.textContent = 'No session';
|
|
statusBadge.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
sessionIdEl.textContent = session.session_id;
|
|
statusBadge.textContent = session.status;
|
|
statusBadge.className = `badge ${session.status}`;
|
|
statusBadge.style.display = '';
|
|
}
|
|
|
|
function renderContextDebugHTML(data) {
|
|
const css = `
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #e6edf3; padding: 24px; font-size: 14px; }
|
|
h1 { font-size: 18px; margin-bottom: 16px; color: #58a6ff; }
|
|
h2 { font-size: 15px; margin: 20px 0 10px; color: #bc8cff; border-bottom: 1px solid #30363d; padding-bottom: 6px; }
|
|
h3 { font-size: 13px; margin: 12px 0 6px; color: #d29922; }
|
|
.meta { color: #8b949e; font-size: 12px; margin-bottom: 16px; }
|
|
.build { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
.build-header { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
|
.tag { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
|
|
.tag.agent { background: rgba(63,185,80,.15); color: #3fb950; }
|
|
.tag.tokens { background: rgba(88,166,255,.15); color: #58a6ff; }
|
|
.tag.sections { background: rgba(188,140,255,.15); color: #bc8cff; }
|
|
.tag.compacted { background: rgba(248,81,73,.15); color: #f85149; }
|
|
.section-row { display: grid; grid-template-columns: 140px 60px 60px 1fr; gap: 8px; padding: 6px 8px; border-radius: 4px; font-size: 12px; align-items: start; }
|
|
.section-row:nth-child(odd) { background: rgba(255,255,255,.02); }
|
|
.section-row .type { color: #58a6ff; font-weight: 600; font-family: 'SF Mono', monospace; }
|
|
.section-row .num { color: #8b949e; text-align: right; font-family: 'SF Mono', monospace; }
|
|
.section-row .preview { color: #8b949e; font-size: 11px; line-height: 1.4; word-break: break-all; }
|
|
.section-header { display: grid; grid-template-columns: 140px 60px 60px 1fr; gap: 8px; padding: 4px 8px; font-size: 11px; color: #6e7681; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid #21262d; margin-bottom: 4px; }
|
|
.bar { height: 6px; border-radius: 3px; margin-top: 8px; background: #21262d; overflow: hidden; }
|
|
.bar-fill { height: 100%; border-radius: 3px; }
|
|
.user-msg { background: #1c2128; border: 1px solid #30363d; border-radius: 6px; padding: 10px 14px; font-size: 12px; font-family: 'SF Mono', monospace; color: #e6edf3; margin-top: 8px; white-space: pre-wrap; word-break: break-word; }
|
|
.empty { color: #6e7681; font-style: italic; padding: 40px; text-align: center; }
|
|
</style>
|
|
`;
|
|
|
|
if (!data.history || data.history.length === 0) {
|
|
return css + '<body><h1>Context Debug</h1><div class="empty">No context builds yet. Send a message first.</div></body>';
|
|
}
|
|
|
|
let html = css + '<body>';
|
|
html += '<h1>Context Engine Debug</h1>';
|
|
html += '<div class="meta">Session: ' + data.session_id + ' — ' + data.total_builds + ' context build(s)</div>';
|
|
|
|
// Reverse to show most recent first
|
|
const builds = [...data.history].reverse();
|
|
|
|
for (let i = 0; i < builds.length; i++) {
|
|
const b = builds[i];
|
|
const time = new Date(b.timestamp * 1000).toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 2 });
|
|
const maxTokens = 120000;
|
|
const pct = Math.min(100, (b.total_tokens / maxTokens) * 100);
|
|
const barColor = pct > 80 ? '#f85149' : pct > 50 ? '#d29922' : '#3fb950';
|
|
|
|
html += '<div class="build">';
|
|
html += '<div class="build-header">';
|
|
html += '<span class="tag agent">' + b.agent + '</span>';
|
|
html += '<span class="tag tokens">~' + b.total_tokens.toLocaleString() + ' tokens</span>';
|
|
html += '<span class="tag sections">' + b.sections_count + ' sections</span>';
|
|
if (b.compacted) html += '<span class="tag compacted">COMPACTED</span>';
|
|
html += '<span style="color:#6e7681;font-size:11px;margin-left:auto">' + time + '</span>';
|
|
html += '</div>';
|
|
|
|
// Token usage bar
|
|
html += '<div class="bar"><div class="bar-fill" style="width:' + pct.toFixed(1) + '%;background:' + barColor + '"></div></div>';
|
|
html += '<div style="font-size:11px;color:#6e7681;margin-top:2px">' + pct.toFixed(1) + '% of context window (' + b.total_tokens.toLocaleString() + ' / ' + maxTokens.toLocaleString() + ')</div>';
|
|
|
|
// Sections table
|
|
html += '<h3>Sections</h3>';
|
|
html += '<div class="section-header"><span>Type</span><span style="text-align:right">Tokens</span><span style="text-align:right">Prio</span><span>Preview</span></div>';
|
|
for (const s of b.sections) {
|
|
html += '<div class="section-row">';
|
|
html += '<span class="type">' + s.type + '</span>';
|
|
html += '<span class="num">' + s.tokens + '</span>';
|
|
html += '<span class="num">' + s.priority + '</span>';
|
|
html += '<span class="preview">' + escapeHtml(s.preview) + '</span>';
|
|
html += '</div>';
|
|
}
|
|
|
|
// User message
|
|
html += '<h3>User Message Sent to Model</h3>';
|
|
html += '<div class="user-msg">' + escapeHtml(b.user_message_preview) + '</div>';
|
|
|
|
// Extra info
|
|
html += '<div style="margin-top:10px;font-size:11px;color:#6e7681">';
|
|
html += 'Artifacts: ' + b.artifacts_count + ' | Working items: ' + b.working_items_count + ' | System prompt tokens: ' + b.system_prompt_tokens;
|
|
html += '</div>';
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</body>';
|
|
return html;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|