(function() {
'use strict';
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;
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');
const leafId = urlLeafId || defaultLeafId;
const byId = new Map();
for (const entry of entries) {
byId.set(entry.id, entry);
}
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 });
}
}
}
}
}
const labelMap = new Map();
for (const entry of entries) {
if (entry.type === 'label' && entry.targetId && entry.label) {
labelMap.set(entry.targetId, entry.label);
}
}
function buildTree() {
const nodeMap = new Map();
const roots = [];
for (const entry of entries) {
nodeMap.set(entry.id, {
entry,
children: [],
label: labelMap.get(entry.id)
});
}
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);
}
}
}
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;
}
function buildActivePathIds(targetId) {
const ids = new Set();
let current = byId.get(targetId);
while (current) {
ids.add(current.id);
if (!current.parentId || current.parentId === current.id) {
break;
}
current = byId.get(current.parentId);
}
return ids;
}
function getPath(targetId) {
const path = [];
let current = byId.get(targetId);
while (current) {
path.unshift(current);
if (!current.parentId || current.parentId === current.id) {
break;
}
current = byId.get(current.parentId);
}
return path;
}
let treeNodeMap = null;
function findNewestLeaf(nodeId) {
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;
let current = node;
while (current.children.length > 0) {
current = current.children[current.children.length - 1];
}
return current.entry.id;
}
function flattenTree(roots, activePathIds) {
const result = [];
const multipleRoots = roots.length > 1;
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);
const stack = [];
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;
const orderedChildren = [...children].sort((a, b) =>
Number(containsActive.get(b)) - Number(containsActive.get(a))
);
let childIndent;
if (multipleChildren) {
childIndent = indent + 1;
} else if (justBranched && indent > 0) {
childIndent = indent + 1;
} else {
childIndent = indent;
}
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;
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;
}
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('');
}
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 parseSkillBlock(text) {
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
if (!match) return null;
return {
name: match[1],
location: match[2],
content: match[3],
userMessage: match[4]?.trim() || undefined,
};
}
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();
}
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;
if (isCurrentLeaf) return true;
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;
}
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: passesFilter = !isSettingsEntry;
break;
}
if (!passesFilter) return false;
if (searchTokens.length > 0) {
const nodeText = getSearchableText(entry, label);
if (!searchTokens.every(t => nodeText.includes(t))) return false;
}
return true;
});
recalculateVisualStructure(filtered, flatNodes);
return filtered;
}
function recalculateVisualStructure(filteredNodes, allFlatNodes) {
if (filteredNodes.length === 0) return;
const visibleIds = new Set(filteredNodes.map(n => n.node.entry.id));
const entryMap = new Map();
for (const flatNode of allFlatNodes) {
entryMap.set(flatNode.node.entry.id, flatNode);
}
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;
}
const visibleParent = new Map();
const visibleChildren = new Map();
visibleChildren.set(null, []);
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);
}
const visibleRootIds = visibleChildren.get(null);
const multipleRoots = visibleRootIds.length > 1;
const filteredNodeMap = new Map();
for (const flatNode of filteredNodes) {
filteredNodeMap.set(flatNode.node.entry.id, flatNode);
}
const 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;
flatNode.indent = indent;
flatNode.showConnector = showConnector;
flatNode.isLast = isLast;
flatNode.gutters = gutters;
flatNode.isVirtualRootChild = isVirtualRootChild;
flatNode.multipleRoots = multipleRoots;
const children = visibleChildren.get(nodeId) || [];
const multipleChildren = children.length > 1;
let childIndent;
if (multipleChildren) {
childIndent = indent + 1;
} else if (justBranched && indent > 0) {
childIndent = indent + 1;
} else {
childIndent = indent;
}
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;
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
]);
}
}
}
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) {
return String(text)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function sanitizeMarkdownUrl(value) {
const href = String(value || '').trim().replace(/[\x00-\x1f\x7f]/g, '');
if (!href) return href;
const scheme = href.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
if (scheme && !/^(https?|mailto|tel|ftp)$/i.test(scheme[1])) {
return null;
}
return href;
}
function truncate(s, maxLen = 100) {
if (s.length <= maxLen) return s;
return s.slice(0, maxLen) + '...';
}
function getTreeNodeDisplayHtml(entry, label) {
const normalize = s => s.replace(/[\n\t]/g, ' ').trim();
const labelHtml = label ? `<span class="tree-label">[${escapeHtml(label)}]</span> ` : '';
switch (entry.type) {
case 'message': {
const msg = entry.message;
if (msg.role === 'user') {
const rawContent = extractContent(msg.content);
const skillBlock = parseSkillBlock(rawContent);
if (skillBlock) {
let treeHtml = labelHtml + `<span class="tree-role-skill">skill:</span> ${escapeHtml(skillBlock.name)}`;
if (skillBlock.userMessage) {
treeHtml += ` · <span class="tree-role-user">user:</span> ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`;
}
return treeHtml;
}
const content = truncate(normalize(rawContent));
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
}
if (msg.role === 'assistant') {
const textContent = truncate(normalize(extractContent(msg.content)));
if (textContent) {
return labelHtml + `<span class="tree-role-assistant">assistant:</span> ${escapeHtml(textContent)}`;
}
if (msg.stopReason === 'aborted') {
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-muted">(aborted)</span>`;
}
if (msg.errorMessage) {
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-error">${escapeHtml(truncate(msg.errorMessage))}</span>`;
}
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-muted">(no text)</span>`;
}
if (msg.role === 'toolResult') {
const toolCall = msg.toolCallId ? toolCallMap.get(msg.toolCallId) : null;
if (toolCall) {
return labelHtml + `<span class="tree-role-tool">${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}</span>`;
}
return labelHtml + `<span class="tree-role-tool">[${escapeHtml(msg.toolName || 'tool')}]</span>`;
}
if (msg.role === 'bashExecution') {
const cmd = truncate(normalize(msg.command || ''));
return labelHtml + `<span class="tree-role-tool">[bash]:</span> ${escapeHtml(cmd)}`;
}
return labelHtml + `<span class="tree-muted">[${escapeHtml(msg.role)}]</span>`;
}
case 'compaction':
return labelHtml + `<span class="tree-compaction">[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]</span>`;
case 'branch_summary': {
const summary = truncate(normalize(entry.summary || ''));
return labelHtml + `<span class="tree-branch-summary">[branch summary]:</span> ${escapeHtml(summary)}`;
}
case 'custom_message': {
const content = typeof entry.content === 'string' ? entry.content : extractContent(entry.content);
return labelHtml + `<span class="tree-custom">[${escapeHtml(entry.customType)}]:</span> ${escapeHtml(truncate(normalize(content)))}`;
}
case 'model_change':
return labelHtml + `<span class="tree-muted">[model: ${escapeHtml(entry.modelId)}]</span>`;
case 'thinking_level_change':
return labelHtml + `<span class="tree-muted">[thinking: ${escapeHtml(entry.thinkingLevel)}]</span>`;
default:
return labelHtml + `<span class="tree-muted">[${escapeHtml(entry.type)}]</span>`;
}
}
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');
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);
div.addEventListener('click', () => {
if (window.getSelection().toString()) return;
const leafId = findNewestLeaf(entry.id);
navigateTo(leafId, 'target', entry.id);
});
container.appendChild(div);
}
treeRendered = true;
} else {
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`;
setTimeout(() => {
const activeNode = container.querySelector('.tree-node.active');
if (activeNode) {
activeNode.scrollIntoView({ block: 'nearest' });
}
}, 0);
}
function forceTreeRerender() {
treeRendered = false;
renderTree();
}
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, ' ');
}
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 `<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
<div class="output-preview"><pre><code class="hljs">${previewHighlighted}</code></pre>
<div class="expand-hint">... (${remaining} more lines)</div></div>
<div class="output-full"><pre><code class="hljs">${highlighted}</code></pre></div></div>`;
}
return `<div class="tool-output"><pre><code class="hljs">${highlighted}</code></pre></div>`;
}
if (remaining > 0) {
let out = '<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle(\'expanded\')">';
out += '<div class="output-preview">';
for (const line of displayLines) {
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
}
out += `<div class="expand-hint">... (${remaining} more lines)</div></div>`;
out += '<div class="output-full">';
for (const line of lines) {
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
}
out += '</div></div>';
return out;
}
let out = '<div class="tool-output">';
for (const line of displayLines) {
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
}
out += '</div>';
return out;
}
function renderToolCall(call) {
const result = findToolResult(call.id);
const isError = result?.isError || false;
const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
const getResultText = () => {
if (!result) return '';
const textBlocks = result.content.filter(c => c.type === 'text');
return textBlocks.map(c => c.text).join('\n');
};
const getResultImages = () => {
if (!result) return [];
return result.content.filter(c => c.type === 'image');
};
const renderResultImages = () => {
const images = getResultImages();
if (images.length === 0) return '';
return '<div class="tool-images">' +
images.map(img => `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="tool-image" />`).join('') +
'</div>';
};
const toolDomId = `tool-call-${escapeHtml(call.id)}`;
let html = `<div class="tool-execution ${statusClass}" id="${toolDomId}">`;
const args = call.arguments || {};
const name = call.name;
const invalidArg = '<span class="tool-error">[invalid arg]</span>';
switch (name) {
case 'bash': {
const command = str(args.command);
const cmdDisplay = command === null ? invalidArg : escapeHtml(command || '...');
html += `<div class="tool-command">$ ${cmdDisplay}</div>`;
if (result) {
const output = getResultText().trim();
if (output) html += formatExpandableOutput(output, 5);
}
break;
}
case 'read': {
const filePath = str(args.file_path ?? args.path);
const offset = args.offset;
const limit = args.limit;
let pathHtml = filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''));
if (filePath !== null && (offset !== undefined || limit !== undefined)) {
const startLine = offset ?? 1;
const endLine = limit !== undefined ? startLine + limit - 1 : '';
pathHtml += `<span class="line-numbers">:${startLine}${endLine ? '-' + endLine : ''}</span>`;
}
html += `<div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">${pathHtml}</span></div>`;
if (result) {
html += renderResultImages();
const output = getResultText();
const lang = filePath ? getLanguageFromPath(filePath) : null;
if (output) html += formatExpandableOutput(output, 10, lang);
}
break;
}
case 'write': {
const filePath = str(args.file_path ?? args.path);
const content = str(args.content);
html += `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span>`;
if (content !== null && content) {
const lines = content.split('\n');
if (lines.length > 10) html += ` <span class="line-count">(${lines.length} lines)</span>`;
}
html += '</div>';
if (content === null) {
html += `<div class="tool-error">[invalid content arg - expected string]</div>`;
} else if (content) {
const lang = filePath ? getLanguageFromPath(filePath) : null;
html += formatExpandableOutput(content, 10, lang);
}
if (result) {
const output = getResultText().trim();
if (output) html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
}
break;
}
case 'edit': {
const filePath = str(args.file_path ?? args.path);
html += `<div class="tool-header"><span class="tool-name">edit</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span></div>`;
if (result?.details?.diff) {
const diffLines = result.details.diff.split('\n');
html += '<div class="tool-diff">';
for (const line of diffLines) {
const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context';
html += `<div class="${cls}">${escapeHtml(replaceTabs(line))}</div>`;
}
html += '</div>';
} else if (result) {
const output = getResultText().trim();
if (output) html += `<div class="tool-output"><pre>${escapeHtml(output)}</pre></div>`;
}
break;
}
case 'ls': {
const dirPath = str(args.path);
const limit = args.limit;
let pathHtml = dirPath === null ? invalidArg : escapeHtml(shortenPath(dirPath || '.'));
if (limit !== undefined) {
pathHtml += ` <span class="line-count">(limit ${escapeHtml(String(limit))})</span>`;
}
html += `<div class="tool-header"><span class="tool-name">ls</span> <span class="tool-path">${pathHtml}</span></div>`;
if (result) {
const output = getResultText().trim();
if (output) html += formatExpandableOutput(output, 20);
}
break;
}
default: {
const rendered = renderedTools?.[call.id];
if (rendered?.callHtml || rendered?.resultHtmlCollapsed || rendered?.resultHtmlExpanded) {
if (rendered.callHtml) {
html += `<div class="tool-header ansi-rendered">${rendered.callHtml}</div>`;
} else {
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
}
if (rendered.resultHtmlCollapsed && rendered.resultHtmlExpanded && rendered.resultHtmlCollapsed !== rendered.resultHtmlExpanded) {
html += `<div class="tool-output expandable ansi-rendered" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
<div class="output-preview">${rendered.resultHtmlCollapsed}</div>
<div class="output-full">${rendered.resultHtmlExpanded}</div>
</div>`;
} else if (rendered.resultHtmlExpanded) {
html += `<div class="tool-output ansi-rendered">${rendered.resultHtmlExpanded}</div>`;
} else if (result) {
const output = getResultText();
if (output) html += formatExpandableOutput(output, 10);
}
} else {
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
html += `<div class="tool-output"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;
if (result) {
const output = getResultText();
if (output) html += formatExpandableOutput(output, 10);
}
}
}
}
html += '</div>';
return html;
}
window.downloadSessionJson = function() {
const lines = [];
if (header) {
lines.push(JSON.stringify({ type: 'header', ...header }));
}
for (const entry of entries) {
lines.push(JSON.stringify(entry));
}
const jsonlContent = lines.join('\n');
const blob = new Blob([jsonlContent], { type: 'application/x-ndjson' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${header?.id || 'session'}.jsonl`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function buildShareUrl(entryId) {
const baseUrlMeta = document.querySelector('meta[name="pi-share-base-url"]');
const baseUrl = baseUrlMeta ? baseUrlMeta.content : window.location.href.split('?')[0];
const url = new URL(window.location.href);
const gistId = Array.from(url.searchParams.keys()).find(k => !url.searchParams.get(k));
const params = new URLSearchParams();
params.set('leafId', currentLeafId);
params.set('targetId', entryId);
if (baseUrlMeta) {
return `${baseUrl}&${params.toString()}`;
}
url.search = gistId ? `?${gistId}&${params.toString()}` : `?${params.toString()}`;
return url.toString();
}
async function copyToClipboard(text, button) {
let success = false;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
success = true;
}
} catch (err) {
}
if (!success) {
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
success = document.execCommand('copy');
document.body.removeChild(textarea);
} catch (err) {
console.error('Failed to copy:', err);
}
}
if (success && button) {
const originalHtml = button.innerHTML;
button.innerHTML = '✓';
button.classList.add('copied');
setTimeout(() => {
button.innerHTML = originalHtml;
button.classList.remove('copied');
}, 1500);
}
}
function renderCopyLinkButton(entryId) {
return `<button class="copy-link-btn" data-entry-id="${escapeHtml(entryId)}" title="Copy link to this message">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</button>`;
}
function renderEntry(entry) {
const ts = formatTimestamp(entry.timestamp);
const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
const entryDomId = `entry-${escapeHtml(entry.id)}`;
const copyBtnHtml = renderCopyLinkButton(entry.id);
if (entry.type === 'message') {
const msg = entry.message;
if (msg.role === 'user') {
const content = msg.content;
const text = typeof content === 'string' ? content :
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
const skillBlock = parseSkillBlock(text);
if (skillBlock) {
const images = Array.isArray(content) ? content.filter(c => c.type === 'image') : [];
const hasUserContent = skillBlock.userMessage || images.length > 0;
let html = `<div class="skill-user-entry" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
html += `<div class="skill-invocation" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
<div class="skill-invocation-label">[skill] ${escapeHtml(skillBlock.name)}</div>
<div class="skill-invocation-collapsed">${escapeHtml(skillBlock.name)} (click to expand)</div>
<div class="skill-invocation-content markdown-content">${safeMarkedParse(skillBlock.content)}</div>
</div>`;
if (hasUserContent) {
html += '<div class="user-message">';
if (images.length > 0) {
html += '<div class="message-images">';
for (const img of images) {
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
}
html += '</div>';
}
if (skillBlock.userMessage) {
html += `<div class="markdown-content">${safeMarkedParse(skillBlock.userMessage)}</div>`;
}
html += '</div>';
}
html += '</div>';
return html;
}
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
if (Array.isArray(content)) {
const images = content.filter(c => c.type === 'image');
if (images.length > 0) {
html += '<div class="message-images">';
for (const img of images) {
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
}
html += '</div>';
}
}
if (text.trim()) {
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
}
html += '</div>';
return html;
}
if (msg.role === 'assistant') {
let html = `<div class="assistant-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
for (const block of msg.content) {
if (block.type === 'text' && block.text.trim()) {
html += `<div class="assistant-text markdown-content">${safeMarkedParse(block.text)}</div>`;
} else if (block.type === 'thinking' && block.thinking.trim()) {
html += `<div class="thinking-block">
<div class="thinking-text">${escapeHtml(block.thinking)}</div>
<div class="thinking-collapsed">Thinking ...</div>
</div>`;
}
}
for (const block of msg.content) {
if (block.type === 'toolCall') {
html += renderToolCall(block);
}
}
if (msg.stopReason === 'aborted') {
html += '<div class="error-text">Aborted</div>';
} else if (msg.stopReason === 'error') {
html += `<div class="error-text">Error: ${escapeHtml(msg.errorMessage || 'Unknown error')}</div>`;
}
html += '</div>';
return html;
}
if (msg.role === 'bashExecution') {
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
let html = `<div class="tool-execution ${isError ? 'error' : 'success'}" id="${entryDomId}">${tsHtml}`;
html += `<div class="tool-command">$ ${escapeHtml(msg.command)}</div>`;
if (msg.output) html += formatExpandableOutput(msg.output, 10);
if (msg.cancelled) {
html += '<div style="color: var(--warning)">(cancelled)</div>';
} else if (msg.exitCode !== 0 && msg.exitCode !== null) {
html += `<div style="color: var(--error)">(exit ${msg.exitCode})</div>`;
}
html += '</div>';
return html;
}
if (msg.role === 'toolResult') return '';
}
if (entry.type === 'model_change') {
return `<div class="model-change" id="${entryDomId}">${tsHtml}Switched to model: <span class="model-name">${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}</span></div>`;
}
if (entry.type === 'compaction') {
return `<div class="compaction" id="${entryDomId}" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
<div class="compaction-label">[compaction]</div>
<div class="compaction-collapsed">Compacted from ${entry.tokensBefore.toLocaleString()} tokens</div>
<div class="compaction-content"><strong>Compacted from ${entry.tokensBefore.toLocaleString()} tokens</strong>\n\n${escapeHtml(entry.summary)}</div>
</div>`;
}
if (entry.type === 'branch_summary') {
return `<div class="branch-summary" id="${entryDomId}">${tsHtml}
<div class="branch-summary-header">Branch Summary</div>
<div class="markdown-content">${safeMarkedParse(entry.summary)}</div>
</div>`;
}
if (entry.type === 'custom_message' && entry.display) {
return `<div class="hook-message" id="${entryDomId}">${tsHtml}
<div class="hook-type">[${escapeHtml(entry.customType)}]</div>
<div class="markdown-content">${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}</div>
</div>`;
}
return '';
}
function computeStats(entryList) {
let userMessages = 0, assistantMessages = 0, toolResults = 0;
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const models = new Set();
for (const entry of entryList) {
if (entry.type === 'message') {
const msg = entry.message;
if (msg.role === 'user') userMessages++;
if (msg.role === 'assistant') {
assistantMessages++;
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
if (msg.usage) {
tokens.input += msg.usage.input || 0;
tokens.output += msg.usage.output || 0;
tokens.cacheRead += msg.usage.cacheRead || 0;
tokens.cacheWrite += msg.usage.cacheWrite || 0;
if (msg.usage.cost) {
cost.input += msg.usage.cost.input || 0;
cost.output += msg.usage.cost.output || 0;
cost.cacheRead += msg.usage.cost.cacheRead || 0;
cost.cacheWrite += msg.usage.cost.cacheWrite || 0;
}
}
toolCalls += msg.content.filter(c => c.type === 'toolCall').length;
}
if (msg.role === 'toolResult') toolResults++;
} else if (entry.type === 'compaction') {
compactions++;
} else if (entry.type === 'branch_summary') {
branchSummaries++;
} else if (entry.type === 'custom_message') {
customMessages++;
}
}
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
}
const globalStats = computeStats(entries);
function renderHeader() {
const totalCost = globalStats.cost.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite;
const tokenParts = [];
if (globalStats.tokens.input) tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`);
if (globalStats.tokens.output) tokenParts.push(`↓${formatTokens(globalStats.tokens.output)}`);
if (globalStats.tokens.cacheRead) tokenParts.push(`R${formatTokens(globalStats.tokens.cacheRead)}`);
if (globalStats.tokens.cacheWrite) tokenParts.push(`W${formatTokens(globalStats.tokens.cacheWrite)}`);
const msgParts = [];
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
if (globalStats.compactions) msgParts.push(`${globalStats.compactions} compactions`);
if (globalStats.branchSummaries) msgParts.push(`${globalStats.branchSummaries} branch summaries`);
let html = `
<div class="header">
<h1>Session: ${escapeHtml(header?.id || 'unknown')}</h1>
<div class="help-bar">
<span class="help-hint">T toggle thinking · O toggle tools</span>
<div class="help-actions">
<button type="button" class="header-toggle-btn" data-action="toggle-thinking" title="Toggle thinking (T)">Toggle thinking</button>
<button type="button" class="header-toggle-btn" data-action="toggle-tools" title="Toggle tools (O)">Toggle tools</button>
<button type="button" class="download-json-btn" onclick="downloadSessionJson()" title="Download session as JSONL">↓ JSONL</button>
</div>
</div>
<div class="header-info">
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}</span></div>
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${escapeHtml(globalStats.models.join(', ') || 'unknown')}</span></div>
<div class="info-item"><span class="info-label">Messages:</span><span class="info-value">${msgParts.join(', ') || '0'}</span></div>
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">${globalStats.toolCalls}</span></div>
<div class="info-item"><span class="info-label">Tokens:</span><span class="info-value">${tokenParts.join(' ') || '0'}</span></div>
<div class="info-item"><span class="info-label">Cost:</span><span class="info-value">$${totalCost.toFixed(3)}</span></div>
</div>
</div>`;
if (systemPrompt) {
const lines = systemPrompt.split('\n');
const previewLines = 10;
if (lines.length > previewLines) {
const preview = lines.slice(0, previewLines).join('\n');
const remaining = lines.length - previewLines;
html += `<div class="system-prompt expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
<div class="system-prompt-header">System Prompt</div>
<div class="system-prompt-preview">${escapeHtml(preview)}</div>
<div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
<div class="system-prompt-full">${escapeHtml(systemPrompt)}</div>
</div>`;
} else {
html += `<div class="system-prompt">
<div class="system-prompt-header">System Prompt</div>
<div class="system-prompt-full" style="display: block">${escapeHtml(systemPrompt)}</div>
</div>`;
}
}
if (tools && tools.length > 0) {
html += `<div class="tools-list">
<div class="tools-header">Available Tools</div>
<div class="tools-content">
${tools.map(t => {
const hasParams = t.parameters && typeof t.parameters === 'object' && t.parameters.properties && Object.keys(t.parameters.properties).length > 0;
if (!hasParams) {
return `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span></div>`;
}
const params = t.parameters;
const properties = params.properties;
const required = params.required || [];
let paramsHtml = '';
for (const [name, prop] of Object.entries(properties)) {
const isRequired = required.includes(name);
const typeStr = prop.type || 'any';
const reqLabel = isRequired ? '<span class="tool-param-required">required</span>' : '<span class="tool-param-optional">optional</span>';
paramsHtml += `<div class="tool-param"><span class="tool-param-name">${escapeHtml(name)}</span> <span class="tool-param-type">${escapeHtml(typeStr)}</span> ${reqLabel}`;
if (prop.description) {
paramsHtml += `<div class="tool-param-desc">${escapeHtml(prop.description)}</div>`;
}
paramsHtml += `</div>`;
}
return `<div class="tool-item" onclick="if(window.getSelection().toString())return;this.classList.toggle('params-expanded')"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span> <span class="tool-params-hint"></span><div class="tool-params-content">${paramsHtml}</div></div>`;
}).join('')}
</div>
</div>`;
}
return html;
}
const entryCache = new Map();
function getScrollTargetElementId(entryId) {
const entry = byId.get(entryId);
if (entry?.type === 'message' && entry.message.role === 'toolResult' && entry.message.toolCallId) {
return `tool-call-${entry.message.toolCallId}`;
}
return `entry-${entryId}`;
}
function renderEntryToNode(entry) {
if (entryCache.has(entry.id)) {
return entryCache.get(entry.id).cloneNode(true);
}
const html = renderEntry(entry);
if (!html) return null;
const template = document.createElement('template');
template.innerHTML = html;
const node = template.content.firstElementChild;
if (node) {
entryCache.set(entry.id, node.cloneNode(true));
}
return node;
}
function navigateTo(targetId, scrollMode = 'target', scrollToEntryId = null) {
currentLeafId = targetId;
currentTargetId = scrollToEntryId || targetId;
const path = getPath(targetId);
renderTree();
document.getElementById('header-container').innerHTML = renderHeader();
attachHeaderHandlers();
const messagesEl = document.getElementById('messages');
const fragment = document.createDocumentFragment();
for (const entry of path) {
const node = renderEntryToNode(entry);
if (node) {
fragment.appendChild(node);
}
}
messagesEl.innerHTML = '';
messagesEl.appendChild(fragment);
messagesEl.querySelectorAll('.copy-link-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const entryId = btn.dataset.entryId;
const shareUrl = buildShareUrl(entryId);
copyToClipboard(shareUrl, btn);
});
});
setTimeout(() => {
const content = document.getElementById('content');
if (scrollMode === 'bottom') {
content.scrollTop = content.scrollHeight;
} else if (scrollMode === 'target') {
const scrollTargetId = scrollToEntryId || targetId;
const targetEl = document.getElementById(getScrollTargetElementId(scrollTargetId)) ||
document.getElementById(`entry-${scrollTargetId}`);
if (targetEl) {
targetEl.scrollIntoView({ block: 'center' });
if (scrollToEntryId) {
targetEl.classList.add('highlight');
setTimeout(() => targetEl.classList.remove('highlight'), 2000);
}
}
}
}, 0);
}
const strictStrikethroughRegex = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
marked.use({
breaks: true,
gfm: true,
tokenizer: {
html() {
return undefined;
},
tag() {
return undefined;
},
del(src) {
const match = strictStrikethroughRegex.exec(src);
if (!match) return undefined;
return {
type: 'del',
raw: match[0],
text: match[2],
tokens: this.lexer.inlineTokens(match[2])
};
}
},
renderer: {
link(token) {
const href = sanitizeMarkdownUrl(token.href);
if (href === null) {
return this.parser.parseInline(token.tokens);
}
let out = '<a href="' + escapeHtml(href) + '"';
if (token.title) {
out += ' title="' + escapeHtml(token.title) + '"';
}
out += '>' + this.parser.parseInline(token.tokens) + '</a>';
return out;
},
image(token) {
const href = sanitizeMarkdownUrl(token.href);
if (href === null) {
return escapeHtml(token.text || '');
}
let out = '<img src="' + escapeHtml(href) + '" alt="' + escapeHtml(token.text || '') + '"';
if (token.title) {
out += ' title="' + escapeHtml(token.title) + '"';
}
out += '>';
return out;
},
code(token) {
const code = token.text;
const lang = token.lang;
let highlighted;
if (lang && hljs.getLanguage(lang)) {
try {
highlighted = hljs.highlight(code, { language: lang }).value;
} catch {
highlighted = escapeHtml(code);
}
} else {
try {
highlighted = hljs.highlightAuto(code).value;
} catch {
highlighted = escapeHtml(code);
}
}
return `<pre><code class="hljs">${highlighted}</code></pre>`;
},
codespan(token) {
return `<code>${escapeHtml(token.text)}</code>`;
}
}
});
function safeMarkedParse(text) {
return marked.parse(text);
}
const searchInput = document.getElementById('tree-search');
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value;
forceTreeRerender();
});
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();
});
});
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
const hamburger = document.getElementById('hamburger');
const sidebarResizer = document.getElementById('sidebar-resizer');
const SIDEBAR_WIDTH_STORAGE_KEY = 'pi-share:v1:sidebar-width';
const MIN_CONTENT_WIDTH = 320;
function isMobileLayout() {
return window.matchMedia('(max-width: 900px)').matches;
}
function getSidebarBounds() {
const rootStyles = getComputedStyle(document.documentElement);
const minWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-min-width')) || 240;
const maxWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-max-width')) || 720;
const viewportMaxWidth = window.innerWidth - MIN_CONTENT_WIDTH;
return {
minWidth,
maxWidth: Math.max(minWidth, Math.min(maxWidth, viewportMaxWidth))
};
}
function clampSidebarWidth(width) {
const { minWidth, maxWidth } = getSidebarBounds();
return Math.max(minWidth, Math.min(maxWidth, width));
}
function applySidebarWidth(width) {
document.documentElement.style.setProperty('--sidebar-width', `${Math.round(clampSidebarWidth(width))}px`);
}
function loadSidebarWidth() {
try {
const raw = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
if (raw === null) return null;
const width = Number(raw);
return Number.isFinite(width) ? width : null;
} catch {
return null;
}
}
function saveSidebarWidth(width) {
try {
localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(clampSidebarWidth(width))));
} catch {
}
}
function setupSidebarResize() {
const savedWidth = loadSidebarWidth();
if (savedWidth !== null) {
applySidebarWidth(savedWidth);
}
if (!sidebarResizer) return;
let cleanupDrag = null;
const stopDrag = (pointerId) => {
if (cleanupDrag) {
cleanupDrag(pointerId);
cleanupDrag = null;
}
};
sidebarResizer.addEventListener('pointerdown', (e) => {
if (isMobileLayout()) return;
e.preventDefault();
const startX = e.clientX;
const startWidth = sidebar.getBoundingClientRect().width;
document.body.classList.add('sidebar-resizing');
sidebarResizer.setPointerCapture?.(e.pointerId);
const onPointerMove = (event) => {
applySidebarWidth(startWidth + (event.clientX - startX));
};
cleanupDrag = (pointerIdToRelease) => {
document.body.classList.remove('sidebar-resizing');
sidebarResizer.releasePointerCapture?.(pointerIdToRelease);
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerCancel);
saveSidebarWidth(sidebar.getBoundingClientRect().width);
};
const onPointerUp = (event) => stopDrag(event.pointerId);
const onPointerCancel = (event) => stopDrag(event.pointerId);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerCancel);
});
sidebarResizer.addEventListener('dblclick', () => {
if (isMobileLayout()) return;
applySidebarWidth(400);
saveSidebarWidth(400);
});
window.addEventListener('resize', () => {
if (isMobileLayout()) return;
applySidebarWidth(sidebar.getBoundingClientRect().width);
});
}
setupSidebarResize();
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);
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);
});
document.querySelectorAll('.skill-invocation').forEach(el => {
el.classList.toggle('expanded', toolOutputsExpanded);
});
};
const attachHeaderHandlers = () => {
document.querySelector('[data-action="toggle-thinking"]')?.addEventListener('click', toggleThinking);
document.querySelector('[data-action="toggle-tools"]')?.addEventListener('click', toggleToolOutputs);
};
const isEditableTarget = (element) => {
if (!element) return false;
const tagName = element.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'BUTTON') {
return true;
}
return element.isContentEditable || Boolean(element.closest?.('[contenteditable="true"]'));
};
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
searchQuery = '';
navigateTo(leafId, 'bottom');
}
if (isEditableTarget(document.activeElement)) {
return;
}
const key = e.key.toLowerCase();
if (key === 't') {
e.preventDefault();
toggleThinking();
} else if (key === 'o') {
e.preventDefault();
toggleToolOutputs();
}
});
if (leafId) {
if (urlTargetId && byId.has(urlTargetId)) {
navigateTo(leafId, 'target', urlTargetId);
} else {
navigateTo(leafId, 'none');
}
} else if (entries.length > 0) {
navigateTo(entries[entries.length - 1].id, 'none');
}
})();