- Introduced a new `execute_code` tool that allows the agent to run Python scripts that call Hermes tools via RPC, reducing the number of round trips required for tool interactions. - Added configuration options for timeout and maximum tool calls in the sandbox environment. - Updated the toolset definitions to include the new code execution capabilities, ensuring integration across platforms. - Implemented comprehensive tests for the code execution sandbox, covering various scenarios including tool call limits and error handling. - Enhanced the CLI and documentation to reflect the new functionality, providing users with clear guidance on using the code execution tool.
683 lines
18 KiB
HTML
683 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Hermes Agent - Session Viewer</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface2: #1c2333;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-muted: #8b949e;
|
|
--accent: #58a6ff;
|
|
--accent-dim: #1f3a5f;
|
|
--user: #da8ee7;
|
|
--user-bg: #2d1b3d;
|
|
--assistant: #58a6ff;
|
|
--assistant-bg: #152238;
|
|
--tool: #3fb950;
|
|
--tool-bg: #12261e;
|
|
--system: #d29922;
|
|
--system-bg: #2a2000;
|
|
--error: #f85149;
|
|
--meta: #768390;
|
|
--radius: 10px;
|
|
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
|
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-sans);
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
display: flex;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Sidebar */
|
|
#sidebar {
|
|
width: 340px;
|
|
min-width: 340px;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#sidebar-header {
|
|
padding: 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#sidebar-header h1 {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
margin-bottom: 4px;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
#sidebar-header p {
|
|
color: var(--text-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
#file-picker {
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#file-picker label {
|
|
display: block;
|
|
padding: 10px 16px;
|
|
background: var(--accent-dim);
|
|
border: 1px dashed var(--accent);
|
|
border-radius: var(--radius);
|
|
text-align: center;
|
|
cursor: pointer;
|
|
color: var(--accent);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
#file-picker label:hover {
|
|
background: #1a4478;
|
|
}
|
|
|
|
#file-picker input { display: none; }
|
|
|
|
#session-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
|
|
.session-item {
|
|
padding: 12px 14px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background 0.12s;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.session-item:hover { background: var(--surface2); }
|
|
.session-item.active { background: var(--accent-dim); border: 1px solid var(--accent); }
|
|
|
|
.session-item .session-title {
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
margin-bottom: 3px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.session-item .session-meta {
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.session-item .session-meta .badge {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-cli { background: #1a3a2a; color: #3fb950; }
|
|
.badge-telegram { background: #1a2a3a; color: #58a6ff; }
|
|
.badge-discord { background: #2a1a3a; color: #bc8cff; }
|
|
|
|
/* Main area */
|
|
#main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#session-header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--surface);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#session-header h2 {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
#session-header .meta-row {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
#session-header .meta-row span { display: flex; align-items: center; gap: 4px; }
|
|
|
|
#messages-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
/* Welcome state */
|
|
#welcome {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
#welcome .icon { font-size: 48px; opacity: 0.3; }
|
|
#welcome h3 { font-size: 18px; color: var(--text); font-weight: 600; }
|
|
|
|
/* Messages */
|
|
.message {
|
|
margin-bottom: 16px;
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.message-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 14px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.message-body {
|
|
padding: 12px 16px;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
font-size: 13.5px;
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.msg-user .message-header { background: var(--user-bg); color: var(--user); }
|
|
.msg-user .message-body { background: #1e1228; }
|
|
.msg-user { border-color: #3d2650; }
|
|
|
|
.msg-assistant .message-header { background: var(--assistant-bg); color: var(--assistant); }
|
|
.msg-assistant .message-body { background: #0f1a2e; }
|
|
.msg-assistant { border-color: #1e3a5f; }
|
|
|
|
.msg-tool .message-header { background: var(--tool-bg); color: var(--tool); }
|
|
.msg-tool .message-body { background: #0c1a14; font-family: var(--font-mono); font-size: 12px; }
|
|
.msg-tool { border-color: #1a3525; }
|
|
|
|
.msg-session_meta .message-header { background: var(--system-bg); color: var(--system); }
|
|
.msg-session_meta .message-body { background: #1a1800; }
|
|
.msg-session_meta { border-color: #3a3000; }
|
|
|
|
.msg-system .message-header { background: var(--system-bg); color: var(--system); }
|
|
.msg-system .message-body { background: #1a1800; }
|
|
.msg-system { border-color: #3a3000; }
|
|
|
|
.tool-calls-section {
|
|
margin-top: 8px;
|
|
border-top: 1px solid var(--border);
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.tool-call-item {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
margin-bottom: 6px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tool-call-name {
|
|
padding: 6px 10px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--tool);
|
|
background: var(--tool-bg);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.tool-call-args {
|
|
padding: 8px 10px;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
color: var(--text-muted);
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* System prompt collapsible */
|
|
.system-prompt-toggle {
|
|
padding: 10px 16px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 16px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.system-prompt-toggle summary {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--system);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.system-prompt-toggle summary::before {
|
|
content: '\25B6';
|
|
font-size: 10px;
|
|
transition: transform 0.15s;
|
|
}
|
|
|
|
.system-prompt-toggle[open] summary::before {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.system-prompt-content {
|
|
margin-top: 10px;
|
|
padding: 12px;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
color: var(--text-muted);
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.timestamp {
|
|
font-size: 11px;
|
|
color: var(--meta);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.tool-result-truncated {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
::-webkit-scrollbar { width: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
|
|
|
.no-content { color: var(--text-muted); font-style: italic; font-size: 12px; }
|
|
|
|
.reasoning-block {
|
|
margin-top: 8px;
|
|
padding: 8px 12px;
|
|
background: #1a1a2e;
|
|
border: 1px solid #2a2a4e;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
color: #a0a0d0;
|
|
white-space: pre-wrap;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.reasoning-label {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: #7070b0;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.session-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin: 24px 0;
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.session-divider::before, .session-divider::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: 16px;
|
|
padding: 8px 14px;
|
|
background: var(--surface2);
|
|
border-radius: 6px;
|
|
margin-bottom: 16px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stats-bar .stat { display: flex; align-items: center; gap: 4px; }
|
|
.stats-bar .stat-label { color: var(--text-muted); }
|
|
.stats-bar .stat-value { color: var(--text); font-weight: 600; font-family: var(--font-mono); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="sidebar">
|
|
<div id="sidebar-header">
|
|
<h1>Hermes Agent</h1>
|
|
<p>Session Transcript Viewer</p>
|
|
</div>
|
|
<div id="file-picker">
|
|
<label for="jsonl-input">Load .jsonl file</label>
|
|
<input type="file" id="jsonl-input" accept=".jsonl,.json,.txt">
|
|
</div>
|
|
<div id="session-list"></div>
|
|
</div>
|
|
|
|
<div id="main">
|
|
<div id="session-header" style="display:none"></div>
|
|
<div id="messages-container">
|
|
<div id="welcome">
|
|
<div class="icon">⚙</div>
|
|
<h3>Load a session file</h3>
|
|
<p>Select a .jsonl file from the sidebar to view exported Hermes Agent sessions.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const sessions = [];
|
|
let activeIdx = -1;
|
|
|
|
document.getElementById('jsonl-input').addEventListener('change', e => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = ev => {
|
|
sessions.length = 0;
|
|
const lines = ev.target.result.split('\n').filter(l => l.trim());
|
|
for (const line of lines) {
|
|
try { sessions.push(JSON.parse(line)); } catch {}
|
|
}
|
|
renderSessionList();
|
|
if (sessions.length > 0) selectSession(0);
|
|
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded from ${file.name}`;
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
|
|
function renderSessionList() {
|
|
const list = document.getElementById('session-list');
|
|
list.innerHTML = '';
|
|
sessions.forEach((s, i) => {
|
|
const firstUserMsg = (s.messages || []).find(m => m.role === 'user');
|
|
const preview = firstUserMsg
|
|
? firstUserMsg.content.substring(0, 80).replace(/\n/g, ' ')
|
|
: '(no messages)';
|
|
|
|
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
|
|
const dateStr = dt ? dt.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
|
|
|
|
const div = document.createElement('div');
|
|
div.className = 'session-item' + (i === activeIdx ? ' active' : '');
|
|
div.onclick = () => selectSession(i);
|
|
div.innerHTML = `
|
|
<div class="session-title">${esc(preview)}</div>
|
|
<div class="session-meta">
|
|
<span class="badge badge-${s.source || 'cli'}">${s.source || 'cli'}</span>
|
|
<span>${dateStr}</span>
|
|
<span>${s.message_count || 0} msgs</span>
|
|
</div>
|
|
`;
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function selectSession(idx) {
|
|
activeIdx = idx;
|
|
const s = sessions[idx];
|
|
|
|
document.querySelectorAll('.session-item').forEach((el, i) => {
|
|
el.classList.toggle('active', i === idx);
|
|
});
|
|
|
|
const header = document.getElementById('session-header');
|
|
header.style.display = 'block';
|
|
|
|
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
|
|
const endDt = s.ended_at ? new Date(s.ended_at * 1000) : null;
|
|
const duration = s.started_at && s.ended_at
|
|
? formatDuration(s.ended_at - s.started_at)
|
|
: 'unknown';
|
|
|
|
header.innerHTML = `
|
|
<h2>Session ${esc(s.id)}</h2>
|
|
<div class="meta-row">
|
|
<span>📡 ${esc(s.source || 'cli')}</span>
|
|
<span>🤖 ${esc(s.model || 'unknown')}</span>
|
|
<span>💬 ${s.message_count || 0} messages</span>
|
|
<span>🔧 ${s.tool_call_count || 0} tool calls</span>
|
|
<span>⏱ ${duration}</span>
|
|
${s.end_reason ? `<span>🏁 ${esc(s.end_reason)}</span>` : ''}
|
|
${dt ? `<span>📅 ${dt.toLocaleString()}</span>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
renderMessages(s);
|
|
}
|
|
|
|
function renderMessages(session) {
|
|
const container = document.getElementById('messages-container');
|
|
container.innerHTML = '';
|
|
|
|
// System prompt (collapsible)
|
|
if (session.system_prompt) {
|
|
const details = document.createElement('details');
|
|
details.className = 'system-prompt-toggle';
|
|
details.innerHTML = `
|
|
<summary>System Prompt (${(session.system_prompt.length / 1024).toFixed(1)}KB)</summary>
|
|
<div class="system-prompt-content">${esc(session.system_prompt)}</div>
|
|
`;
|
|
container.appendChild(details);
|
|
}
|
|
|
|
// Stats bar
|
|
const stats = document.createElement('div');
|
|
stats.className = 'stats-bar';
|
|
stats.innerHTML = `
|
|
<div class="stat"><span class="stat-label">Messages:</span><span class="stat-value">${session.message_count || 0}</span></div>
|
|
<div class="stat"><span class="stat-label">Tool Calls:</span><span class="stat-value">${session.tool_call_count || 0}</span></div>
|
|
<div class="stat"><span class="stat-label">Source:</span><span class="stat-value">${esc(session.source || 'cli')}</span></div>
|
|
${session.user_id ? `<div class="stat"><span class="stat-label">User ID:</span><span class="stat-value">${esc(session.user_id)}</span></div>` : ''}
|
|
`;
|
|
container.appendChild(stats);
|
|
|
|
const messages = session.messages || [];
|
|
for (const msg of messages) {
|
|
const el = renderMessage(msg);
|
|
container.appendChild(el);
|
|
}
|
|
|
|
container.scrollTop = 0;
|
|
}
|
|
|
|
function renderMessage(msg) {
|
|
const div = document.createElement('div');
|
|
const role = msg.role || 'unknown';
|
|
div.className = `message msg-${role}`;
|
|
|
|
const roleIcon = {
|
|
user: '👤',
|
|
assistant: '🤖',
|
|
tool: '🔧',
|
|
session_meta: '⚙',
|
|
system: '📋'
|
|
}[role] || '❓';
|
|
|
|
const ts = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString() : '';
|
|
const toolName = msg.tool_name ? ` (${msg.tool_name})` : '';
|
|
|
|
let headerExtra = '';
|
|
if (msg.tool_call_id && role === 'tool') {
|
|
headerExtra = ` — <span style="opacity:0.7;font-size:10px;text-transform:none;letter-spacing:0">${esc(msg.tool_call_id.substring(0, 24))}...</span>`;
|
|
}
|
|
|
|
div.innerHTML = `<div class="message-header">
|
|
<span>${roleIcon}</span>
|
|
<span>${role}${toolName}</span>
|
|
${headerExtra}
|
|
<span style="margin-left:auto" class="timestamp">${ts}</span>
|
|
</div>`;
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'message-body';
|
|
|
|
// Content
|
|
if (msg.content) {
|
|
let text = msg.content;
|
|
// Try to detect if content is a JSON string and pretty-print it
|
|
if (role === 'tool' && text.startsWith('{')) {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
text = JSON.stringify(parsed, null, 2);
|
|
} catch {}
|
|
}
|
|
const contentDiv = document.createElement('div');
|
|
if (role === 'tool') {
|
|
contentDiv.className = 'tool-result-truncated';
|
|
}
|
|
contentDiv.textContent = text;
|
|
body.appendChild(contentDiv);
|
|
} else if (role !== 'session_meta' && !msg.tool_calls) {
|
|
const empty = document.createElement('span');
|
|
empty.className = 'no-content';
|
|
empty.textContent = '(no text content)';
|
|
body.appendChild(empty);
|
|
}
|
|
|
|
// Reasoning
|
|
if (msg.reasoning) {
|
|
const rBlock = document.createElement('div');
|
|
rBlock.innerHTML = `<div class="reasoning-label">Reasoning</div>`;
|
|
const rContent = document.createElement('div');
|
|
rContent.className = 'reasoning-block';
|
|
rContent.textContent = msg.reasoning;
|
|
rBlock.appendChild(rContent);
|
|
body.appendChild(rBlock);
|
|
}
|
|
|
|
// Tool calls
|
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
const tcSection = document.createElement('div');
|
|
tcSection.className = 'tool-calls-section';
|
|
const label = document.createElement('div');
|
|
label.style.cssText = 'font-size:11px;font-weight:700;color:var(--tool);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;';
|
|
label.textContent = `Tool Calls (${msg.tool_calls.length})`;
|
|
tcSection.appendChild(label);
|
|
|
|
for (const tc of msg.tool_calls) {
|
|
const fn = tc.function || {};
|
|
const tcItem = document.createElement('div');
|
|
tcItem.className = 'tool-call-item';
|
|
|
|
const nameDiv = document.createElement('div');
|
|
nameDiv.className = 'tool-call-name';
|
|
nameDiv.textContent = fn.name || 'unknown';
|
|
tcItem.appendChild(nameDiv);
|
|
|
|
if (fn.arguments) {
|
|
const argsDiv = document.createElement('div');
|
|
argsDiv.className = 'tool-call-args';
|
|
let argsText = fn.arguments;
|
|
try {
|
|
argsText = JSON.stringify(JSON.parse(fn.arguments), null, 2);
|
|
} catch {}
|
|
argsDiv.textContent = argsText;
|
|
tcItem.appendChild(argsDiv);
|
|
}
|
|
|
|
tcSection.appendChild(tcItem);
|
|
}
|
|
body.appendChild(tcSection);
|
|
}
|
|
|
|
div.appendChild(body);
|
|
return div;
|
|
}
|
|
|
|
function esc(str) {
|
|
if (!str) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
return `${h}h ${m}m`;
|
|
}
|
|
|
|
// Auto-load if file is in same directory (for local dev)
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
fetch('exprted.jsonl')
|
|
.then(r => { if (!r.ok) throw new Error(); return r.text(); })
|
|
.then(text => {
|
|
const lines = text.split('\n').filter(l => l.trim());
|
|
for (const line of lines) {
|
|
try { sessions.push(JSON.parse(line)); } catch {}
|
|
}
|
|
if (sessions.length) {
|
|
renderSessionList();
|
|
selectSession(sessions.length - 1);
|
|
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded`;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|