// 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),
});
}
// 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);
// 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,
]);
}
}
/** * Filter flat nodes based on current filterMode and searchQuery.
*/ function filterNodes(flatNodes, currentLeafId) { const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
// 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;
} returnnull;
}
// 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,
]);
}
// 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 escapeHtml(text) { const div = document.createElement("div");
div.textContent = text; return div.innerHTML;
}
function escapeHtmlAttr(text) { return escapeHtml(text).replaceAll('"', """).replaceAll("'", "'");
}
// Validate image fields before interpolating data URLs. const SAFE_IMAGE_MIME_RE = /^image\/(png|jpeg|gif|webp|svg\+xml|bmp|tiff|avif)$/i; const SAFE_BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
function sanitizeImageMimeType(mimeType) { if (typeof mimeType === "string" && SAFE_IMAGE_MIME_RE.test(mimeType)) { return mimeType.toLowerCase();
} return"application/octet-stream";
}
function sanitizeImageBase64(data) { if (typeof data !== "string") { return"";
} const cleaned = data.replace(/\s+/g, ""); if (!cleaned || cleaned.length % 4 !== 0 || !SAFE_BASE64_RE.test(cleaned)) { return"";
} return cleaned;
}
function renderDataUrlImage(img, className) { const mimeType = sanitizeImageMimeType(img?.mimeType); const base64 = sanitizeImageBase64(img?.data); if (!base64) { return"";
} return `<img src="data:${mimeType};base64,${base64}"class="${className}" />`;
} /** * 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 ? `<span class="tree-label">[${escapeHtml(label)}]</span> ` : "";
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;
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"";
} returnnull;
}
// Plain text output if (remaining > 0) {
let out = '<div class="tool-output expandable" onclick="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";
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>`;
} elseif (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>";
} elseif (result) { const output = getResultText().trim(); if (output) {
html += `<div class="tool-output"><pre>${escapeHtml(output)}</pre></div>`;
}
} break;
} default: { // Check for pre-rendered custom tool HTML const rendered = renderedTools?.[call.id]; if (rendered?.callHtml || rendered?.resultHtml) { // Custom tool with pre-rendered HTML from TUI renderer 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.resultHtml) { // Apply same truncation as built-in tools (10 lines) const lines = rendered.resultHtml.split("\n"); if (lines.length > 10) { const preview = lines.slice(0, 10).join("\n");
html += `<div class="tool-output expandable ansi-rendered" onclick="this.classList.toggle('expanded')">
<div class="output-preview">${preview}<div class="expand-hint">... (${lines.length - 10} more lines)</div></div>
<div class="output-full">${rendered.resultHtml}</div>
</div>`;
} else {
html += `<div class="tool-output ansi-rendered">${rendered.resultHtml}</div>`;
}
} elseif (result) { // Fallback to JSON for result if no pre-rendered HTML const output = getResultText(); if (output) {
html += formatExpandableOutput(output, 10);
}
}
} else { // Fallback to JSON display (existing behavior)
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;
}
/** * Download the session data as a JSONL file. * Reconstructs the original format: header line + entry lines.
*/
window.downloadSessionJson = function () { // Build JSONL content: header first, then all entries 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");
/** * Build a shareable URL for a specific message. * URL format: base?gistId&leafId=<leafId>&targetId=<entryId>
*/ function buildShareUrl(entryId) { // Check for injected base URL (used when loaded in iframe via srcdoc) 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); // Find the gist ID (first query param without value, e.g., ?abc123) const gistId = Array.from(url.searchParams.keys()).find((k) => !url.searchParams.get(k));
// Build the share URL const params = new URLSearchParams();
params.set("leafId", currentLeafId);
params.set("targetId", entryId);
// If we have an injected base URL (iframe context), use it directly if (baseUrlMeta) { return `${baseUrl}&${params.toString()}`;
}
// Otherwise build from current location (direct file access)
url.search = gistId ? `?${gistId}&${params.toString()}` : `?${params.toString()}`; return url.toString();
}
/** * Copy text to clipboard with visual feedback. * Uses navigator.clipboard with fallback to execCommand for HTTP contexts.
*/
async function copyToClipboard(text, button) {
let success = false; try { if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
success = true;
}
} catch { // Clipboard API failed, try fallback
}
// Fallback for HTTP or when Clipboard API is unavailable 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);
}
}
// Use setTimeout(0) to ensure DOM is fully laid out before scrolling
setTimeout(() => { const content = document.getElementById("content"); if (scrollMode === "bottom") {
content.scrollTop = content.scrollHeight;
} elseif (scrollMode === "target") { // If scrollToEntryId is provided, scroll to that specific entry const scrollTargetId = scrollToEntryId || targetId; const targetEl = document.getElementById(`entry-${scrollTargetId}`); if (targetEl) {
targetEl.scrollIntoView({ block: "center" }); // Briefly highlight the target message if (scrollToEntryId) {
targetEl.classList.add("highlight");
setTimeout(() => targetEl.classList.remove("highlight"), 2000);
}
}
}
}, 0);
}
// 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");
}
} elseif (entries.length > 0) { // Fallback: use last entry if no leafId
navigateTo(entries[entries.length - 1].id, "none");
}
})();
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.27 Sekunden
(vorverarbeitet am 2026-04-27)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.