Something went wrong. Try again.
I'm going to build my own OpenClaw, with blackjack... and bun!
Something went wrong. Try again.
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Session Tree</title><link rel="stylesheet" href="/static/dist/app.bundle.css"><style>body { font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); font-size: 13px; color: var(--text-primary, #e0e0e0); background: var(--bg-primary, #0f1117); height: 100vh; display: flex; flex-direction: column; overflow: hidden; margin: 0; padding: 0;}.st-header { flex-shrink: 0; padding: 10px 16px; border-bottom: 1px solid var(--border-color, #333); display: flex; align-items: center; gap: 10px; background: var(--bg-secondary, #1a1d28);}.st-header h1 { font-size: 15px; font-weight: 600; flex: 1; margin: 0; }.st-btn { padding: 5px 12px; border-radius: var(--radius-md, 6px); border: 1px solid var(--border-color, #333); background: transparent; color: var(--text-primary, #e0e0e0); cursor: pointer; font: inherit; font-size: 12px;}.st-btn:hover { background: var(--bg-hover, #252540); }.st-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; }.st-tree { padding: 4px 0 80px; }.st-row { display: flex; align-items: center; gap: 0; padding: 2px 12px 2px 0; min-height: 28px; cursor: pointer; transition: background 0.1s;}.st-row:hover { background: var(--bg-hover, #1a1d28); }.st-row.active { background: color-mix(in srgb, var(--accent-color, #1d9bf0) 12%, transparent); }.st-row.selected { outline: 2px solid var(--accent-color, #1d9bf0); outline-offset: -2px; border-radius: var(--radius-md, 6px); }.st-indent { flex-shrink: 0; }.st-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-secondary, #666); flex-shrink: 0; margin: 0 7px 0 3px;}.st-dot.active { background: var(--accent-color, #1d9bf0); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color, #1d9bf0) 25%, transparent); }.st-dot.branch-point { background: var(--accent-color, #1d9bf0); opacity: 0.5; }.st-body { min-width: 0; flex: 1; display: flex; align-items: baseline; gap: 6px; }.st-preview { font-size: 12px; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0;}.st-id { font-family: var(--font-family-mono, monospace); font-size: 9px; color: var(--text-secondary, #888); flex-shrink: 0;}.st-label { display: inline-block; padding: 1px 5px; border-radius: 999px; font-size: 9px; font-weight: 600; background: color-mix(in srgb, var(--accent-color, #1d9bf0) 15%, transparent); color: var(--accent-color, #1d9bf0); flex-shrink: 0;}.st-active-marker { font-size: 9px; color: var(--accent-color, #1d9bf0); font-weight: 600; flex-shrink: 0;}.st-detail { flex-shrink: 0; padding: 10px 16px; background: var(--bg-secondary, #1a1d28); border-top: 1px solid var(--border-color, #333); display: none;}.st-detail.visible { display: block; }.st-detail-id { font-family: var(--font-family-mono, monospace); font-size: 11px; color: var(--text-secondary, #888); margin-bottom: 4px; }.st-detail-preview { font-size: 13px; line-height: 1.4; margin-bottom: 8px; word-break: break-word; }.st-detail-actions { display: flex; gap: 8px; flex-wrap: wrap; }.st-detail-actions .st-btn.primary { border-color: var(--accent-color, #1d9bf0); color: var(--accent-color, #1d9bf0); }.st-info { padding: 6px 16px; font-size: 11px; color: var(--text-secondary, #888); border-bottom: 1px solid var(--border-color, #333); flex-shrink: 0; }.st-empty { padding: 32px; text-align: center; color: var(--text-secondary, #888); }.st-loading { padding: 32px; text-align: center; color: var(--text-secondary, #888); }.st-error { padding: 32px; text-align: center; color: var(--danger-color, #ef4444); }</style><script>// Apply theme before first paint(function() { try { var theme = localStorage.getItem('piclaw_theme') || 'default'; var tint = localStorage.getItem('piclaw_tint') || null; if (theme === 'dark' || theme === 'default') document.documentElement.setAttribute('data-theme', 'dark'); else if (theme === 'light') document.documentElement.setAttribute('data-theme', 'light'); } catch(e) {}})();</script></head><body><div class="st-header"> <h1>Session Tree</h1> <button class="st-btn" onclick="loadTree()">Refresh</button> <button class="st-btn" onclick="closeWidget()">Close</button></div><div id="info-bar" class="st-info" style="display:none"></div><div class="st-scroll" id="tree-scroll"> <div id="tree-root" class="st-tree"><div class="st-loading">Loading…</div></div></div><div id="detail-panel" class="st-detail"> <div class="st-detail-id" id="detail-id"></div> <div class="st-detail-preview" id="detail-preview"></div> <div class="st-detail-actions"> <button class="st-btn primary" onclick="navigateToSelected()">Navigate here</button> <button class="st-btn" onclick="navigateToSelected(true)">Navigate + Summarize</button> </div></div><script>var selectedId = null;
function getChatJid() { try { return new URLSearchParams(window.location.search).get('chat_jid') || ''; } catch(e) { return ''; }}
function closeWidget() { if (window.piclawWidget) window.piclawWidget.close({ reason: 'done' }); else window.close();}
function loadTree() { var root = document.getElementById('tree-root'); root.innerHTML = '<div class="st-loading">Loading\u2026</div>'; var chatJid = getChatJid(); var url = '/agent/session-tree' + (chatJid ? '?chat_jid=' + encodeURIComponent(chatJid) : ''); fetch(url, { credentials: 'same-origin' }) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function(data) { renderTree(data); }) .catch(function(err) { root.innerHTML = '<div class="st-error">Failed to load: ' + (err.message || err) + '</div>'; });}
function buildTreeFromFlat(flatNodes) { var byId = {}, roots = [], i, n, node; for (i = 0; i < flatNodes.length; i++) { n = flatNodes[i]; byId[n.id] = { id: n.id, parentId: n.parentId, type: n.type, timestamp: n.timestamp, label: n.label, active: n.active, preview: n.preview, children: [], depth: 0 }; } for (i = 0; i < flatNodes.length; i++) { n = flatNodes[i]; node = byId[n.id]; if (n.parentId && byId[n.parentId]) { byId[n.parentId].children.push(node); } else { roots.push(node); } } // Compute depths iteratively var stack = []; for (i = roots.length - 1; i >= 0; i--) { roots[i].depth = 0; stack.push(roots[i]); } while (stack.length > 0) { node = stack.pop(); for (i = node.children.length - 1; i >= 0; i--) { node.children[i].depth = node.depth + 1; stack.push(node.children[i]); } } return roots;}
function flattenTree(roots) { var result = [], stack = [], i; for (i = roots.length - 1; i >= 0; i--) stack.push(roots[i]); while (stack.length > 0) { var node = stack.pop(); result.push(node); for (i = (node.children || []).length - 1; i >= 0; i--) stack.push(node.children[i]); } return result;}
function renderTree(data) { var root = document.getElementById('tree-root'); var infoBar = document.getElementById('info-bar'); root.innerHTML = '';
if (!data || !data.nodes || data.nodes.length === 0) { root.innerHTML = '<div class="st-empty">Session tree is empty.</div>'; return; }
if (data.capped) { infoBar.textContent = 'Showing ' + data.nodes.length + ' of ' + data.total + ' entries.'; infoBar.style.display = ''; }
var trees = data.flat ? buildTreeFromFlat(data.nodes) : data.nodes; var flat = flattenTree(trees); var activeRowEl = null;
for (var idx = 0; idx < flat.length; idx++) { var node = flat[idx]; var row = document.createElement('div'); row.className = 'st-row' + (node.active ? ' active' : ''); row.setAttribute('data-id', node.id); row.onclick = (function(n) { return function(e) { selectNode(n, e.currentTarget); }; })(node);
var indent = document.createElement('div'); indent.className = 'st-indent'; indent.style.width = ((node.depth || 0) * 16 + 8) + 'px'; row.appendChild(indent);
var dot = document.createElement('div'); var hasBranch = (node.children || []).length > 1; dot.className = 'st-dot' + (node.active ? ' active' : hasBranch ? ' branch-point' : ''); row.appendChild(dot);
var body = document.createElement('div'); body.className = 'st-body';
var preview = document.createElement('span'); preview.className = 'st-preview'; preview.textContent = node.preview || '[' + (node.type || 'entry') + ']'; body.appendChild(preview);
if (node.label) { var lbl = document.createElement('span'); lbl.className = 'st-label'; lbl.textContent = node.label; body.appendChild(lbl); }
if (node.active) { var marker = document.createElement('span'); marker.className = 'st-active-marker'; marker.textContent = '\u2190 active'; body.appendChild(marker); }
var idSpan = document.createElement('span'); idSpan.className = 'st-id'; idSpan.textContent = node.id.slice(0, 8); body.appendChild(idSpan);
row.appendChild(body); root.appendChild(row); if (node.active) activeRowEl = row; }
if (activeRowEl) { setTimeout(function() { activeRowEl.scrollIntoView({ block: 'center', behavior: 'auto' }); }, 50); }}
function selectNode(node, rowEl) { selectedId = node.id; var prev = document.querySelector('.st-row.selected'); if (prev) prev.classList.remove('selected'); if (rowEl) rowEl.classList.add('selected'); document.getElementById('detail-id').textContent = node.id + (node.active ? ' (active leaf)' : ''); document.getElementById('detail-preview').textContent = node.preview || '[' + (node.type || 'entry') + ']'; document.getElementById('detail-panel').classList.add('visible');}
function navigateToSelected(summarize) { if (!selectedId) return; var cmd = '/tree ' + selectedId; if (summarize) cmd += ' --summarize'; if (window.piclawWidget) { window.piclawWidget.submit({ text: cmd }); window.piclawWidget.close({ reason: 'navigated' }); } else { // Standalone mode: post via API fetch('/agent/default/message', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: cmd }) }).then(function() { window.close(); }).catch(function() {}); }}
loadTree();</script></body></html>