(function() { 'use strict'; // ============================================================ // DATA LOADING // ============================================================ const base64 = document.getElementById('session-data').textContent; const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } const data = JSON.parse(new TextDecoder('utf-8').decode(bytes)); const { header, entries, leafId: defaultLeafId, systemPrompt, tools, renderedTools } = data; // ============================================================ // URL PARAMETER HANDLING // ============================================================ // Parse URL parameters for deep linking: leafId and targetId // Check for injected params (when loaded in iframe via srcdoc) or use window.location const injectedParams = document.querySelector('meta[name="pi-url-params"]'); const searchString = injectedParams ? injectedParams.content : window.location.search.substring(1); const urlParams = new URLSearchParams(searchString); const urlLeafId = urlParams.get('leafId'); const urlTargetId = urlParams.get('targetId'); // Use URL leafId if provided, otherwise fall back to session default const leafId = urlLeafId || defaultLeafId; // ============================================================ // DATA STRUCTURES // ============================================================ // Entry lookup by ID const byId = new Map(); for (const entry of entries) { byId.set(entry.id, entry); } // Tool call lookup (toolCallId -> {name, arguments}) const toolCallMap = new Map(); for (const entry of entries) { if (entry.type === 'message' && entry.message.role === 'assistant') { const content = entry.message.content; if (Array.isArray(content)) { for (const block of content) { if (block.type === 'toolCall') { toolCallMap.set(block.id, { name: block.name, arguments: block.arguments }); } } } } } // Label lookup (entryId -> label string) // Labels are stored in 'label' entries that reference their target via targetId const labelMap = new Map(); for (const entry of entries) { if (entry.type === 'label' && entry.targetId && entry.label) { labelMap.set(entry.targetId, entry.label); } } // ============================================================ // TREE DATA PREPARATION (no DOM, pure data) // ============================================================ /** * Build tree structure from flat entries. * Returns array of root nodes, each with { entry, children, label }. */ function buildTree() { const nodeMap = new Map(); const roots = []; // Create nodes for (const entry of entries) { nodeMap.set(entry.id, { entry, children: [], label: labelMap.get(entry.id) }); } // Build parent-child relationships for (const entry of entries) { const node = nodeMap.get(entry.id); if (entry.parentId === null || entry.parentId === undefined || entry.parentId === entry.id) { roots.push(node); } else { const parent = nodeMap.get(entry.parentId); if (parent) { parent.children.push(node); } else { roots.push(node); } } } // Sort children by timestamp function sortChildren(node) { node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime() ); node.children.forEach(sortChildren); } roots.forEach(sortChildren); return roots; } /** * Build set of entry IDs on path from root to target. */ function buildActivePathIds(targetId) { const ids = new Set(); let current = byId.get(targetId); while (current) { ids.add(current.id); // Stop if no parent or self-referencing (root) if (!current.parentId || current.parentId === current.id) { break; } current = byId.get(current.parentId); } return ids; } /** * Get array of entries from root to target (the conversation path). */ function getPath(targetId) { const path = []; let current = byId.get(targetId); while (current) { path.unshift(current); // Stop if no parent or self-referencing (root) if (!current.parentId || current.parentId === current.id) { break; } current = byId.get(current.parentId); } return path; } // Tree node lookup for finding leaves let treeNodeMap = null; /** * Find the newest leaf node reachable from a given node. * This allows clicking any node in a branch to show the full branch. * Children are sorted by timestamp, so the newest is always last. */ function findNewestLeaf(nodeId) { // Build tree node map lazily if (!treeNodeMap) { treeNodeMap = new Map(); const tree = buildTree(); function mapNodes(node) { treeNodeMap.set(node.entry.id, node); node.children.forEach(mapNodes); } tree.forEach(mapNodes); } const node = treeNodeMap.get(nodeId); if (!node) return nodeId; // Follow the newest (last) child at each level let current = node; while (current.children.length > 0) { current = current.children[current.children.length - 1]; } return current.entry.id; } /** * Flatten tree into list with indentation and connector info. * Returns array of { node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }. * Matches tree-selector.ts logic exactly. */ function flattenTree(roots, activePathIds) { const result = []; const multipleRoots = roots.length > 1; // Mark which subtrees contain the active leaf const containsActive = new Map(); function markActive(node) { let has = activePathIds.has(node.entry.id); for (const child of node.children) { if (markActive(child)) has = true; } containsActive.set(node, has); return has; } roots.forEach(markActive); // Stack: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] const stack = []; // Add roots (prioritize branch containing active leaf) const orderedRoots = [...roots].sort((a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)) ); for (let i = orderedRoots.length - 1; i >= 0; i--) { const isLast = i === orderedRoots.length - 1; stack.push([orderedRoots[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, isLast, [], multipleRoots]); } while (stack.length > 0) { const [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop(); result.push({ node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }); const children = node.children; const multipleChildren = children.length > 1; // Order children (active branch first) const orderedChildren = [...children].sort((a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)) ); // Calculate child indent (matches tree-selector.ts) let childIndent; if (multipleChildren) { // Parent branches: children get +1 childIndent = indent + 1; } else if (justBranched && indent > 0) { // First generation after a branch: +1 for visual grouping childIndent = indent + 1; } else { // Single-child chain: stay flat childIndent = indent; } // Build gutters for children const connectorDisplayed = showConnector && !isVirtualRootChild; const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; const connectorPosition = Math.max(0, currentDisplayIndent - 1); const childGutters = connectorDisplayed ? [...gutters, { position: connectorPosition, show: !isLast }] : gutters; // Add children in reverse order for stack for (let i = orderedChildren.length - 1; i >= 0; i--) { const childIsLast = i === orderedChildren.length - 1; stack.push([orderedChildren[i], childIndent, multipleChildren, multipleChildren, childIsLast, childGutters, false]); } } return result; } /** * Build ASCII prefix string for tree node. */ function buildTreePrefix(flatNode) { const { indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots } = flatNode; const displayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; const connector = showConnector && !isVirtualRootChild ? (isLast ? '└─ ' : '├─ ') : ''; const connectorPosition = connector ? displayIndent - 1 : -1; const totalChars = displayIndent * 3; const prefixChars = []; for (let i = 0; i < totalChars; i++) { const level = Math.floor(i / 3); const posInLevel = i % 3; const gutter = gutters.find(g => g.position === level); if (gutter) { prefixChars.push(posInLevel === 0 ? (gutter.show ? '│' : ' ') : ' '); } else if (connector && level === connectorPosition) { if (posInLevel === 0) { prefixChars.push(isLast ? '└' : '├'); } else if (posInLevel === 1) { prefixChars.push('─'); } else { prefixChars.push(' '); } } else { prefixChars.push(' '); } } return prefixChars.join(''); } // ============================================================ // FILTERING (pure data) // ============================================================ let filterMode = 'default'; let searchQuery = ''; function hasTextContent(content) { if (typeof content === 'string') return content.trim().length > 0; if (Array.isArray(content)) { for (const c of content) { if (c.type === 'text' && c.text && c.text.trim().length > 0) return true; } } return false; } function extractContent(content) { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content .filter(c => c.type === 'text' && c.text) .map(c => c.text) .join(''); } return ''; } function getSearchableText(entry, label) { const parts = []; if (label) parts.push(label); switch (entry.type) { case 'message': { const msg = entry.message; parts.push(msg.role); if (msg.content) parts.push(extractContent(msg.content)); if (msg.role === 'bashExecution' && msg.command) parts.push(msg.command); break; } case 'custom_message': parts.push(entry.customType); parts.push(typeof entry.content === 'string' ? entry.content : extractContent(entry.content)); break; case 'compaction': parts.push('compaction'); break; case 'branch_summary': parts.push('branch summary', entry.summary); break; case 'model_change': parts.push('model', entry.modelId); break; case 'thinking_level_change': parts.push('thinking', entry.thinkingLevel); break; } return parts.join(' ').toLowerCase(); } /** * Filter flat nodes based on current filterMode and searchQuery. */ function filterNodes(flatNodes, currentLeafId) { const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean); const filtered = flatNodes.filter(flatNode => { const entry = flatNode.node.entry; const label = flatNode.node.label; const isCurrentLeaf = entry.id === currentLeafId; // Always show current leaf if (isCurrentLeaf) return true; // Hide assistant messages with only tool calls (no text) unless error/aborted if (entry.type === 'message' && entry.message.role === 'assistant') { const msg = entry.message; const hasText = hasTextContent(msg.content); const isErrorOrAborted = msg.stopReason && msg.stopReason !== 'stop' && msg.stopReason !== 'toolUse'; if (!hasText && !isErrorOrAborted) return false; } // Apply filter mode const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change'].includes(entry.type); let passesFilter = true; switch (filterMode) { case 'user-only': passesFilter = entry.type === 'message' && entry.message.role === 'user'; break; case 'no-tools': passesFilter = !isSettingsEntry && !(entry.type === 'message' && entry.message.role === 'toolResult'); break; case 'labeled-only': passesFilter = label !== undefined; break; case 'all': passesFilter = true; break; default: // 'default' passesFilter = !isSettingsEntry; break; } if (!passesFilter) return false; // Apply search filter if (searchTokens.length > 0) { const nodeText = getSearchableText(entry, label); if (!searchTokens.every(t => nodeText.includes(t))) return false; } return true; }); // Recalculate visual structure based on visible tree recalculateVisualStructure(filtered, flatNodes); return filtered; } /** * Recompute indentation/connectors for the filtered view * * Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor. * Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right. */ function recalculateVisualStructure(filteredNodes, allFlatNodes) { if (filteredNodes.length === 0) return; const visibleIds = new Set(filteredNodes.map(n => n.node.entry.id)); // Build entry map for parent lookup (using full tree) const entryMap = new Map(); for (const flatNode of allFlatNodes) { entryMap.set(flatNode.node.entry.id, flatNode); } // Find nearest visible ancestor for a node function findVisibleAncestor(nodeId) { let currentId = entryMap.get(nodeId)?.node.entry.parentId; while (currentId != null) { if (visibleIds.has(currentId)) { return currentId; } currentId = entryMap.get(currentId)?.node.entry.parentId; } return null; } // Build visible tree structure const visibleParent = new Map(); const visibleChildren = new Map(); visibleChildren.set(null, []); // root-level nodes for (const flatNode of filteredNodes) { const nodeId = flatNode.node.entry.id; const ancestorId = findVisibleAncestor(nodeId); visibleParent.set(nodeId, ancestorId); if (!visibleChildren.has(ancestorId)) { visibleChildren.set(ancestorId, []); } visibleChildren.get(ancestorId).push(nodeId); } // Update multipleRoots based on visible roots const visibleRootIds = visibleChildren.get(null); const multipleRoots = visibleRootIds.length > 1; // Build a map for quick lookup: nodeId → FlatNode const filteredNodeMap = new Map(); for (const flatNode of filteredNodes) { filteredNodeMap.set(flatNode.node.entry.id, flatNode); } // DFS traversal of visible tree, applying same indentation rules as flattenTree() // Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] const stack = []; // Add visible roots in reverse order (to process in forward order via stack) for (let i = visibleRootIds.length - 1; i >= 0; i--) { const isLast = i === visibleRootIds.length - 1; stack.push([ visibleRootIds[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, isLast, [], multipleRoots ]); } while (stack.length > 0) { const [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop(); const flatNode = filteredNodeMap.get(nodeId); if (!flatNode) continue; // Update this node's visual properties flatNode.indent = indent; flatNode.showConnector = showConnector; flatNode.isLast = isLast; flatNode.gutters = gutters; flatNode.isVirtualRootChild = isVirtualRootChild; flatNode.multipleRoots = multipleRoots; // Get visible children of this node const children = visibleChildren.get(nodeId) || []; const multipleChildren = children.length > 1; // Calculate child indent using same rules as flattenTree(): // - Parent branches (multiple children): children get +1 // - Just branched and indent > 0: children get +1 for visual grouping // - Single-child chain: stay flat let childIndent; if (multipleChildren) { childIndent = indent + 1; } else if (justBranched && indent > 0) { childIndent = indent + 1; } else { childIndent = indent; } // Build gutters for children (same logic as flattenTree) const connectorDisplayed = showConnector && !isVirtualRootChild; const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; const connectorPosition = Math.max(0, currentDisplayIndent - 1); const childGutters = connectorDisplayed ? [...gutters, { position: connectorPosition, show: !isLast }] : gutters; // Add children in reverse order (to process in forward order via stack) for (let i = children.length - 1; i >= 0; i--) { const childIsLast = i === children.length - 1; stack.push([ children[i], childIndent, multipleChildren, multipleChildren, childIsLast, childGutters, false ]); } } } // ============================================================ // TREE DISPLAY TEXT (pure data -> string) // ============================================================ function shortenPath(p) { if (typeof p !== 'string') return ''; if (p.startsWith('/Users/')) { const parts = p.split('/'); if (parts.length > 2) return '~' + p.slice(('/Users/' + parts[2]).length); } if (p.startsWith('/home/')) { const parts = p.split('/'); if (parts.length > 2) return '~' + p.slice(('/home/' + parts[2]).length); } return p; } function formatToolCall(name, args) { switch (name) { case 'read': { const path = shortenPath(String(args.path || args.file_path || '')); const offset = args.offset; const limit = args.limit; let display = path; if (offset !== undefined || limit !== undefined) { const start = offset ?? 1; const end = limit !== undefined ? start + limit - 1 : ''; display += `:${start}${end ? `-${end}` : ''}`; } return `[read: ${display}]`; } case 'write': return `[write: ${shortenPath(String(args.path || args.file_path || ''))}]`; case 'edit': return `[edit: ${shortenPath(String(args.path || args.file_path || ''))}]`; case 'bash': { const rawCmd = String(args.command || ''); const cmd = rawCmd.replace(/[\n\t]/g, ' ').trim().slice(0, 50); return `[bash: ${cmd}${rawCmd.length > 50 ? '...' : ''}]`; } case 'grep': return `[grep: /${args.pattern || ''}/ in ${shortenPath(String(args.path || '.'))}]`; case 'find': return `[find: ${args.pattern || ''} in ${shortenPath(String(args.path || '.'))}]`; case 'ls': return `[ls: ${shortenPath(String(args.path || '.'))}]`; default: { const argsStr = JSON.stringify(args).slice(0, 40); return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? '...' : ''}]`; } } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Truncate string to maxLen chars, append "..." if truncated. */ function truncate(s, maxLen = 100) { if (s.length <= maxLen) return s; return s.slice(0, maxLen) + '...'; } /** * Get display text for tree node (returns HTML string). */ function getTreeNodeDisplayHtml(entry, label) { const normalize = s => s.replace(/[\n\t]/g, ' ').trim(); const labelHtml = label ? `[${escapeHtml(label)}] ` : ''; switch (entry.type) { case 'message': { const msg = entry.message; if (msg.role === 'user') { const content = truncate(normalize(extractContent(msg.content))); return labelHtml + `user: ${escapeHtml(content)}`; } if (msg.role === 'assistant') { const textContent = truncate(normalize(extractContent(msg.content))); if (textContent) { return labelHtml + `assistant: ${escapeHtml(textContent)}`; } if (msg.stopReason === 'aborted') { return labelHtml + `assistant: (aborted)`; } if (msg.errorMessage) { return labelHtml + `assistant: ${escapeHtml(truncate(msg.errorMessage))}`; } return labelHtml + `assistant: (no text)`; } if (msg.role === 'toolResult') { const toolCall = msg.toolCallId ? toolCallMap.get(msg.toolCallId) : null; if (toolCall) { return labelHtml + `${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}`; } return labelHtml + `[${msg.toolName || 'tool'}]`; } if (msg.role === 'bashExecution') { const cmd = truncate(normalize(msg.command || '')); return labelHtml + `[bash]: ${escapeHtml(cmd)}`; } return labelHtml + `[${msg.role}]`; } case 'compaction': return labelHtml + `[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]`; case 'branch_summary': { const summary = truncate(normalize(entry.summary || '')); return labelHtml + `[branch summary]: ${escapeHtml(summary)}`; } case 'custom_message': { const content = typeof entry.content === 'string' ? entry.content : extractContent(entry.content); return labelHtml + `[${escapeHtml(entry.customType)}]: ${escapeHtml(truncate(normalize(content)))}`; } case 'model_change': return labelHtml + `[model: ${entry.modelId}]`; case 'thinking_level_change': return labelHtml + `[thinking: ${entry.thinkingLevel}]`; default: return labelHtml + `[${entry.type}]`; } } // ============================================================ // TREE RENDERING (DOM manipulation) // ============================================================ let currentLeafId = leafId; let currentTargetId = urlTargetId || leafId; let treeRendered = false; function renderTree() { const tree = buildTree(); const activePathIds = buildActivePathIds(currentLeafId); const flatNodes = flattenTree(tree, activePathIds); const filtered = filterNodes(flatNodes, currentLeafId); const container = document.getElementById('tree-container'); // Full render only on first call or when filter/search changes if (!treeRendered) { container.innerHTML = ''; for (const flatNode of filtered) { const entry = flatNode.node.entry; const isOnPath = activePathIds.has(entry.id); const isTarget = entry.id === currentTargetId; const div = document.createElement('div'); div.className = 'tree-node'; if (isOnPath) div.classList.add('in-path'); if (isTarget) div.classList.add('active'); div.dataset.id = entry.id; const prefix = buildTreePrefix(flatNode); const prefixSpan = document.createElement('span'); prefixSpan.className = 'tree-prefix'; prefixSpan.textContent = prefix; const marker = document.createElement('span'); marker.className = 'tree-marker'; marker.textContent = isOnPath ? '•' : ' '; const content = document.createElement('span'); content.className = 'tree-content'; content.innerHTML = getTreeNodeDisplayHtml(entry, flatNode.node.label); div.appendChild(prefixSpan); div.appendChild(marker); div.appendChild(content); // Navigate to the newest leaf through this node, but scroll to the clicked node div.addEventListener('click', () => { const leafId = findNewestLeaf(entry.id); navigateTo(leafId, 'target', entry.id); }); container.appendChild(div); } treeRendered = true; } else { // Just update markers and classes const nodes = container.querySelectorAll('.tree-node'); for (const node of nodes) { const id = node.dataset.id; const isOnPath = activePathIds.has(id); const isTarget = id === currentTargetId; node.classList.toggle('in-path', isOnPath); node.classList.toggle('active', isTarget); const marker = node.querySelector('.tree-marker'); if (marker) { marker.textContent = isOnPath ? '•' : ' '; } } } document.getElementById('tree-status').textContent = `${filtered.length} / ${flatNodes.length} entries`; // Scroll active node into view after layout setTimeout(() => { const activeNode = container.querySelector('.tree-node.active'); if (activeNode) { activeNode.scrollIntoView({ block: 'nearest' }); } }, 0); } function forceTreeRerender() { treeRendered = false; renderTree(); } // ============================================================ // MESSAGE RENDERING // ============================================================ function formatTokens(count) { if (count < 1000) return count.toString(); if (count < 10000) return (count / 1000).toFixed(1) + 'k'; if (count < 1000000) return Math.round(count / 1000) + 'k'; return (count / 1000000).toFixed(1) + 'M'; } function formatTimestamp(ts) { if (!ts) return ''; const date = new Date(ts); return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function replaceTabs(text) { return text.replace(/\t/g, ' '); } /** Safely coerce value to string for display. Returns null if invalid type. */ function str(value) { if (typeof value === 'string') return value; if (value == null) return ''; return null; } function getLanguageFromPath(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); const extToLang = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp', php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash', sql: 'sql', html: 'html', css: 'css', scss: 'scss', json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml', md: 'markdown', dockerfile: 'dockerfile' }; return extToLang[ext]; } function findToolResult(toolCallId) { for (const entry of entries) { if (entry.type === 'message' && entry.message.role === 'toolResult') { if (entry.message.toolCallId === toolCallId) { return entry.message; } } } return null; } function formatExpandableOutput(text, maxLines, lang) { text = replaceTabs(text); const lines = text.split('\n'); const displayLines = lines.slice(0, maxLines); const remaining = lines.length - maxLines; if (lang) { let highlighted; try { highlighted = hljs.highlight(text, { language: lang }).value; } catch { highlighted = escapeHtml(text); } if (remaining > 0) { const previewCode = displayLines.join('\n'); let previewHighlighted; try { previewHighlighted = hljs.highlight(previewCode, { language: lang }).value; } catch { previewHighlighted = escapeHtml(previewCode); } return `
`; } return `${highlighted}${escapeHtml(output)}${escapeHtml(JSON.stringify(args, null, 2))}${highlighted}`;
},
// Text content: escape HTML tags
text(token) {
return escapeHtmlTags(escapeHtml(token.text));
},
// Inline code: escape HTML
codespan(token) {
return `${escapeHtml(token.text)}`;
}
}
});
// Simple marked parse (escaping handled in renderers)
function safeMarkedParse(text) {
return marked.parse(text);
}
// Search input
const searchInput = document.getElementById('tree-search');
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value;
forceTreeRerender();
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
filterMode = btn.dataset.filter;
forceTreeRerender();
});
});
// Sidebar toggle
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
const hamburger = document.getElementById('hamburger');
hamburger.addEventListener('click', () => {
sidebar.classList.add('open');
overlay.classList.add('open');
hamburger.style.display = 'none';
});
const closeSidebar = () => {
sidebar.classList.remove('open');
overlay.classList.remove('open');
hamburger.style.display = '';
};
overlay.addEventListener('click', closeSidebar);
document.getElementById('sidebar-close').addEventListener('click', closeSidebar);
// Toggle states
let thinkingExpanded = true;
let toolOutputsExpanded = false;
const toggleThinking = () => {
thinkingExpanded = !thinkingExpanded;
document.querySelectorAll('.thinking-text').forEach(el => {
el.style.display = thinkingExpanded ? '' : 'none';
});
document.querySelectorAll('.thinking-collapsed').forEach(el => {
el.style.display = thinkingExpanded ? 'none' : 'block';
});
};
const toggleToolOutputs = () => {
toolOutputsExpanded = !toolOutputsExpanded;
document.querySelectorAll('.tool-output.expandable').forEach(el => {
el.classList.toggle('expanded', toolOutputsExpanded);
});
document.querySelectorAll('.compaction').forEach(el => {
el.classList.toggle('expanded', toolOutputsExpanded);
});
};
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
searchQuery = '';
navigateTo(leafId, 'bottom');
}
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
toggleThinking();
}
if (e.ctrlKey && e.key === 'o') {
e.preventDefault();
toggleToolOutputs();
}
});
// Initial render
// If URL has targetId, scroll to that specific message; otherwise stay at top
if (leafId) {
if (urlTargetId && byId.has(urlTargetId)) {
// Deep link: navigate to leaf and scroll to target message
navigateTo(leafId, 'target', urlTargetId);
} else {
navigateTo(leafId, 'none');
}
} else if (entries.length > 0) {
// Fallback: use last entry if no leafId
navigateTo(entries[entries.length - 1].id, 'none');
}
})();