Skip to content

Instantly share code, notes, and snippets.

@av
Created June 25, 2026 08:15
Show Gist options
  • Select an option

  • Save av/11d7925e79a8a5fac3d724e8f9353e14 to your computer and use it in GitHub Desktop.

Select an option

Save av/11d7925e79a8a5fac3d724e8f9353e14 to your computer and use it in GitHub Desktop.
grok session — Explain me the tracequest share feature (grok-composer-2.5-fast, 11s)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>tracequest — 019efdd4</title>
<style>
:root {
--bg: #111113;
--surface: #19191c;
--surface2: #222226;
--fg: #e4e4e7;
--fg2: #8b8b92;
--fg3: #5c5c63;
--border: rgba(255, 255, 255, 0.06);
--accent: #8b7cf6;
--red: #f07070;
--green: #4ade80;
--orange: #e8a44c;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;
--green-dim: rgba(74, 222, 128, 0.1);
--red-dim: rgba(240, 112, 112, 0.1);
--orange-dim: rgba(232, 164, 76, 0.1);
--radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
font-size: 14px;
line-height: 1.6;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
#app { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
.header {
background: var(--surface);
border-radius: var(--radius);
padding: 24px 28px;
margin-bottom: 12px;
}
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--fg);
letter-spacing: -0.2px;
margin-bottom: 16px;
display: flex;
align-items: baseline;
gap: 8px;
}
.header-title span {
color: var(--fg3);
font-weight: 400;
font-size: 13px;
font-family: var(--mono);
}
.header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.header-top .header-title { margin-bottom: 0; min-width: 0; }
.export-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg2);
font-size: 12px;
font-family: var(--mono);
padding: 5px 10px;
cursor: pointer;
text-decoration: none;
transition: all 0.12s;
white-space: nowrap;
flex-shrink: 0;
}
.export-btn:hover {
color: var(--fg);
border-color: rgba(255, 255, 255, 0.14);
background: rgba(139, 124, 246, 0.08);
}
.md-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg2);
font-size: 12px;
font-family: var(--mono);
padding: 5px 10px;
cursor: pointer;
text-decoration: none;
transition: all 0.12s;
white-space: nowrap;
flex-shrink: 0;
}
.md-btn:hover {
color: var(--fg);
border-color: rgba(255, 255, 255, 0.14);
background: rgba(139, 124, 246, 0.08);
}
.export-btn svg, .print-btn svg, .md-btn svg, .hf-btn svg {
width: 12px;
height: 12px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
.print-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg2);
font-size: 12px;
font-family: var(--mono);
padding: 5px 10px;
cursor: pointer;
transition: all 0.12s;
white-space: nowrap;
flex-shrink: 0;
}
.print-btn:hover {
color: var(--fg);
border-color: rgba(255, 255, 255, 0.14);
background: rgba(139, 124, 246, 0.08);
}
.hf-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg2);
font-size: 12px;
font-family: var(--mono);
padding: 5px 10px;
cursor: pointer;
transition: all 0.12s;
white-space: nowrap;
flex-shrink: 0;
}
.hf-btn:hover {
color: var(--fg);
border-color: rgba(255, 255, 255, 0.14);
background: rgba(139, 124, 246, 0.08);
}
.hf-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.hf-modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
width: 400px;
max-width: 90vw;
}
.hf-modal-title {
font-size: 15px;
font-weight: 600;
color: var(--fg);
margin-bottom: 16px;
}
.hf-modal-label {
display: block;
font-size: 12px;
color: var(--fg2);
margin-bottom: 4px;
margin-top: 12px;
}
.hf-modal-input {
width: 100%;
box-sizing: border-box;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-family: var(--mono);
font-size: 13px;
padding: 8px 10px;
}
.hf-modal-input:focus {
outline: none;
border-color: rgba(139, 124, 246, 0.5);
}
.hf-modal-hint {
font-size: 11px;
color: var(--fg3);
margin-top: 4px;
}
.hf-modal-status {
font-size: 12px;
color: var(--fg2);
margin-top: 12px;
min-height: 18px;
word-break: break-all;
}
.hf-modal-status.hf-error { color: var(--red, #f07070); }
.hf-modal-status.hf-success { color: var(--green, #59d4a0); }
.hf-modal-status a { color: inherit; text-decoration: underline; }
.hf-modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
}
.hf-modal-share, .hf-modal-cancel {
font-family: var(--mono);
font-size: 12px;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
border: 1px solid var(--border);
transition: all 0.12s;
}
.hf-modal-cancel {
background: var(--surface2);
color: var(--fg2);
}
.hf-modal-share {
background: rgba(139, 124, 246, 0.15);
color: var(--fg);
border-color: rgba(139, 124, 246, 0.3);
}
.hf-modal-share:hover { background: rgba(139, 124, 246, 0.25); }
.hf-modal-share:disabled { opacity: 0.5; cursor: default; }
.header-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.meta-grid {
display: flex;
flex-wrap: wrap;
gap: 6px 20px;
}
.meta-item { display: flex; gap: 6px; align-items: baseline; }
.meta-label { color: var(--fg3); font-size: 12px; font-weight: 500; }
.meta-value {
color: var(--fg2);
font-size: 12px;
font-family: var(--mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stats-bar {
display: flex;
flex-wrap: wrap;
gap: 0;
margin-bottom: 12px;
background: var(--surface);
border-radius: var(--radius);
padding: 16px 0;
}
.stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 12px;
border-right: 1px solid var(--border);
}
.stat:last-child { border-right: none; }
.stats-note {
width: 100%;
text-align: center;
font-size: 11px;
color: var(--fg3);
padding-top: 10px;
border-top: 1px solid var(--border);
margin-top: 10px;
}
.stat-value {
font-size: 22px;
font-weight: 600;
color: var(--fg);
letter-spacing: -0.5px;
font-variant-numeric: tabular-nums;
}
.stat-value.red { color: var(--red); }
.stat-value.green { color: var(--green); }
.stat-label {
font-size: 11px;
color: var(--fg3);
margin-top: 4px;
letter-spacing: 0.3px;
}
.chapters { display: flex; flex-direction: column; gap: 2px; }
.chapter {
background: var(--surface);
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
transition: background 0.15s;
}
.chapter:hover { background: var(--surface2); }
.chapter.expanded { background: var(--surface2); }
.chapter-head {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 20px;
}
.chapter-num {
font-size: 12px;
color: var(--fg3);
min-width: 20px;
padding-top: 2px;
font-variant-numeric: tabular-nums;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.chapter-outcome-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.chapter-outcome-dot.dot-clean { background: var(--green); opacity: 0.6; }
.chapter-outcome-dot.dot-corrected { background: var(--orange); opacity: 0.8; }
.chapter-outcome-dot.dot-struggling { background: var(--red); opacity: 0.9; }
.chapter-body { flex: 1; min-width: 0; }
.chapter-prompt {
font-size: 14px;
color: var(--fg);
line-height: 1.5;
margin-bottom: 8px;
}
.chapter-prompt-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.chapter.expanded .chapter-prompt-text {
display: block;
-webkit-line-clamp: unset;
}
.chapter-meta {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.chapter-tools {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.ch-tool {
padding: 2px 7px;
border-radius: 5px;
font-size: 11px;
font-weight: 500;
font-family: var(--mono);
background: rgba(255, 255, 255, 0.04);
color: var(--fg2);
}
.chapter-outcome {
font-size: 11px;
font-weight: 500;
padding: 2px 7px;
border-radius: 5px;
}
.chapter-outcome.clean { background: var(--green-dim); color: var(--green); }
.chapter-outcome.error { background: var(--red-dim); color: var(--red); }
.chapter-outcome.corrected { background: var(--orange-dim); color: var(--orange); }
.chapter-patterns {
display: inline-flex;
gap: 4px;
align-items: center;
}
.chapter-pattern-badge {
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
font-family: var(--mono);
}
.chapter-pattern-badge.retry {
background: var(--red-dim);
color: var(--red);
}
.chapter-pattern-badge.correction {
background: var(--green-dim);
color: var(--green);
}
.chapter-right {
text-align: right;
white-space: nowrap;
flex-shrink: 0;
padding-top: 2px;
}
.chapter-time {
font-size: 12px;
color: var(--fg3);
font-family: var(--mono);
font-variant-numeric: tabular-nums;
}
.chapter-turns { font-size: 11px; color: var(--fg3); margin-top: 2px; }
.chapter-tokens {
font-size: 11px;
color: var(--fg3);
font-family: var(--mono);
font-variant-numeric: tabular-nums;
margin-top: 2px;
}
.chapter-thinking-badge {
font-size: 10px;
color: var(--accent);
font-family: var(--mono);
opacity: 0.8;
margin-top: 1px;
}
.chapter-token-detail { margin-bottom: 10px; }
.chapter-token-breakdown {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
padding: 2px 0;
font-variant-numeric: tabular-nums;
}
.chapter-token-cost {
font-size: 11px;
font-family: var(--mono);
color: var(--accent);
margin-top: 2px;
}
.chapter-thinking {
margin-bottom: 10px;
border-left: 2px solid rgba(167, 139, 250, 0.3);
padding-left: 10px;
}
.chapter-thinking-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.chapter-thinking-icon {
font-size: 13px;
line-height: 1;
}
.chapter-thinking-count {
font-size: 10px;
font-family: var(--mono);
color: var(--fg3);
margin-left: auto;
font-variant-numeric: tabular-nums;
}
.chapter-thinking-block {
font-size: 11px;
font-family: var(--mono);
font-style: italic;
color: var(--fg3);
background: rgba(167, 139, 250, 0.04);
border-radius: 4px;
padding: 6px 8px;
margin-bottom: 4px;
max-height: 72px;
overflow: hidden;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.4;
position: relative;
}
.chapter-thinking-block.expanded {
max-height: none;
overflow: visible;
}
.chapter-thinking-more {
font-size: 10px;
color: var(--fg3);
font-style: italic;
padding: 2px 0;
}
.chapter-permalink {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 4px;
color: var(--fg3);
opacity: 0;
transition: opacity 0.15s, background 0.15s, color 0.15s;
cursor: pointer;
font-size: 12px;
margin-top: 2px;
}
.chapter:hover .chapter-permalink,
.chapter.expanded .chapter-permalink { opacity: 1; }
.chapter-permalink:hover {
background: rgba(139, 124, 246, 0.12);
color: var(--accent);
}
.chapter-permalink.copied {
opacity: 1;
color: var(--green);
}
.chapter-detail {
padding: 0 20px 16px 52px;
display: none;
}
.chapter.expanded .chapter-detail { display: block; }
.chapter-quality {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 6px;
background: var(--surface);
border-left: 3px solid var(--fg3);
}
.chapter-quality-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
margin-bottom: 4px;
}
.chapter-quality-label.corrected { color: var(--orange); border-color: var(--orange); }
.chapter-quality-label.struggling { color: var(--red); border-color: var(--red); }
.chapter-quality-label.clean { color: var(--green); }
.chapter-quality .chapter-quality-label.corrected ~ * { border-color: var(--orange); }
.chapter-quality:has(.chapter-quality-label.corrected) { border-left-color: var(--orange); }
.chapter-quality:has(.chapter-quality-label.struggling) { border-left-color: var(--red); }
.chapter-quality-retries {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.chapter-retry-chip {
font-size: 11px;
font-family: var(--mono);
color: var(--fg2);
background: rgba(240, 112, 112, 0.06);
padding: 2px 7px;
border-radius: 4px;
}
.chapter-quality-corrections {
font-size: 11px;
color: var(--green);
margin-top: 4px;
}
.chapter-waste-badge {
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
font-family: var(--mono);
background: var(--orange-dim);
color: var(--orange);
}
.chapter-efficiency {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 6px;
background: var(--surface);
border-left: 3px solid var(--orange);
}
.chapter-efficiency-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.chapter-efficiency-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
color: var(--orange);
}
.chapter-efficiency-score {
font-size: 11px;
font-family: var(--mono);
color: var(--fg2);
margin-left: auto;
}
.chapter-efficiency-bar-wrap {
height: 4px;
background: rgba(255,255,255,0.06);
border-radius: 2px;
margin-bottom: 8px;
}
.chapter-efficiency-bar {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.chapter-efficiency-bar.eff-good { background: var(--green); }
.chapter-efficiency-bar.eff-ok { background: var(--orange); }
.chapter-efficiency-bar.eff-bad { background: var(--red); }
.chapter-efficiency-metrics {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
}
.chapter-efficiency-metric {
font-size: 11px;
font-family: var(--mono);
color: var(--fg2);
}
.chapter-efficiency-metric-value {
font-weight: 600;
color: var(--fg);
}
.chapter-efficiency-metric-value.val-good { color: var(--green); }
.chapter-efficiency-metric-value.val-bad { color: var(--red); }
.chapter-efficiency-reasons {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.chapter-efficiency-reason {
font-size: 10px;
padding: 2px 7px;
border-radius: 4px;
background: rgba(232,164,76,0.08);
color: var(--orange);
}
.chapter-files { margin-bottom: 10px; }
.chapter-files-label {
font-size: 11px;
color: var(--fg3);
font-weight: 500;
margin-bottom: 4px;
}
.chapter-file {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
padding: 2px 0;
display: flex;
gap: 8px;
align-items: baseline;
}
.chapter-file-ops {
font-size: 10px;
color: var(--fg3);
}
.chapter-response {
font-size: 13px;
color: var(--fg3);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
border-top: 1px solid var(--border);
padding-top: 10px;
margin-top: 10px;
}
.chapter-commands { margin-bottom: 10px; }
.chapter-cmd {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
padding: 2px 0;
display: flex;
gap: 6px;
align-items: baseline;
}
.chapter-cmd-status { font-size: 11px; }
.chapter-cmd-status.ok { color: var(--green); }
.chapter-cmd-status.fail { color: var(--red); }
.chapter-output {
font-size: 11px;
font-family: var(--mono);
color: var(--fg3);
background: rgba(255, 255, 255, 0.02);
border-radius: 4px;
padding: 4px 8px;
margin: 2px 0 6px 18px;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
max-height: 80px;
overflow: hidden;
border-left: 2px solid rgba(255, 255, 255, 0.06);
position: relative;
}
.chapter-output.expanded, .chapter-diff-del.expanded,
.chapter-diff-add.expanded, .chapter-agent-prompt.expanded,
.chapter-mcp-output.expanded,
.chapter-thinking-block.expanded {
max-height: none;
overflow: visible;
}
.expand-toggle {
display: block;
font-size: 10px;
font-family: var(--mono);
color: var(--accent);
cursor: pointer;
padding: 2px 8px;
margin: -2px 0 6px 18px;
user-select: none;
opacity: 0.8;
transition: opacity 0.15s;
}
.expand-toggle:hover { opacity: 1; }
.expand-toggle.in-diff {
margin: -2px 0 2px 0;
padding: 2px 8px;
}
.chapter-search-count {
font-size: 10px;
color: var(--fg3);
margin-left: 8px;
white-space: nowrap;
}
.chapter-web { margin-bottom: 10px; }
.chapter-web-label {
font-size: 11px;
font-weight: 600;
color: var(--fg3);
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 5px;
}
.chapter-web-label::before {
content: '\1F310';
font-size: 12px;
}
.chapter-web-op {
font-size: 12px;
color: var(--fg2);
padding: 6px 8px;
margin-bottom: 6px;
background: var(--surface2);
border-radius: 6px;
border-left: 3px solid #6ba4e8;
}
.chapter-web-op.error { border-left-color: var(--red); }
.chapter-web-type {
font-size: 10px;
font-family: var(--mono);
font-weight: 600;
text-transform: uppercase;
padding: 1px 5px;
border-radius: 3px;
margin-right: 6px;
}
.chapter-web-type.fetch {
color: #6ba4e8;
background: rgba(107, 164, 232, 0.12);
}
.chapter-web-type.search {
color: #a78bfa;
background: rgba(167, 139, 250, 0.12);
}
.chapter-web-url {
font-family: var(--mono);
font-size: 11px;
color: #6ba4e8;
text-decoration: none;
word-break: break-all;
}
.chapter-web-url:hover { text-decoration: underline; }
.chapter-web-query {
font-weight: 500;
color: var(--fg);
}
.chapter-web-prompt {
font-size: 11px;
color: var(--fg3);
margin-top: 3px;
font-style: italic;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.chapter-web-preview {
font-size: 11px;
font-family: var(--mono);
color: var(--fg2);
margin-top: 4px;
padding: 4px 6px;
background: rgba(255, 255, 255, 0.02);
border-radius: 4px;
max-height: 48px;
overflow: hidden;
white-space: pre-wrap;
position: relative;
}
.chapter-web-results {
margin-top: 4px;
padding-left: 4px;
}
.chapter-web-result {
font-size: 11px;
padding: 1px 0;
display: flex;
align-items: baseline;
gap: 6px;
}
.chapter-web-result-title {
color: var(--fg);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
.chapter-web-result-url {
font-family: var(--mono);
font-size: 10px;
color: var(--fg3);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.chapter-web-result-url:hover { color: #6ba4e8; }
.chapter-web-count {
font-size: 10px;
color: var(--fg3);
margin-left: auto;
white-space: nowrap;
}
.chapter-web-title {
font-size: 11px;
color: var(--fg);
font-weight: 500;
margin-top: 2px;
}
.chapter-agents { margin-bottom: 10px; }
.chapter-agent {
font-size: 12px;
color: var(--fg2);
padding: 2px 0;
display: flex;
gap: 6px;
align-items: baseline;
}
.chapter-agent-status { font-size: 11px; }
.chapter-agent-status.ok { color: var(--green); }
.chapter-agent-status.fail { color: var(--red); }
.chapter-agent-status.pending { color: var(--fg3); }
.chapter-agent-desc {
font-size: 12px;
color: var(--fg);
font-weight: 500;
}
.chapter-agent-type {
font-size: 10px;
padding: 1px 5px;
border-radius: 4px;
background: rgba(167, 139, 250, 0.12);
color: var(--accent);
font-family: var(--mono);
}
.chapter-agent-prompt {
font-size: 11px;
font-family: var(--mono);
color: var(--fg3);
padding: 3px 8px;
margin: 2px 0 4px 18px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
border-left: 2px solid rgba(167, 139, 250, 0.2);
max-height: 60px;
overflow: hidden;
position: relative;
}
.chapter-mcp { margin-bottom: 10px; }
.chapter-mcp-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--fg3);
margin-bottom: 6px;
}
.chapter-mcp-label::before {
content: '\2699 ';
font-size: 12px;
}
.chapter-mcp-op {
font-size: 12px;
padding: 6px 8px;
margin-bottom: 4px;
background: var(--surface2);
border-radius: 6px;
border-left: 3px solid #5dadec;
}
.chapter-mcp-op.mcp-error { border-left-color: var(--red); }
.chapter-mcp-header {
display: flex;
align-items: baseline;
gap: 6px;
flex-wrap: wrap;
}
.chapter-mcp-server {
font-size: 10px;
font-family: var(--mono);
font-weight: 600;
color: #5dadec;
background: rgba(93, 173, 236, 0.12);
padding: 1px 6px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.chapter-mcp-tool {
font-size: 12px;
font-family: var(--mono);
color: var(--fg);
font-weight: 500;
}
.chapter-mcp-status {
font-size: 11px;
margin-left: auto;
}
.chapter-mcp-status.ok { color: var(--green); }
.chapter-mcp-status.fail { color: var(--red); }
.chapter-mcp-params {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
margin-top: 4px;
font-size: 11px;
}
.chapter-mcp-param-key {
color: var(--fg3);
font-family: var(--mono);
}
.chapter-mcp-param-val {
color: var(--fg2);
font-family: var(--mono);
word-break: break-all;
}
.chapter-mcp-output {
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
background: rgba(255,255,255,0.02);
border-radius: 4px;
padding: 4px 6px;
margin-top: 4px;
max-height: 48px;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
position: relative;
}
.chapter-mcp-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-family: var(--mono);
color: #5dadec;
background: rgba(93, 173, 236, 0.08);
border-radius: 4px;
padding: 1px 6px;
margin-left: 4px;
white-space: nowrap;
}
.chapter-diffs { margin-bottom: 10px; }
.chapter-diff-header {
display: flex;
gap: 8px;
align-items: baseline;
margin-top: 6px;
margin-bottom: 2px;
}
.chapter-diff-path {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
}
.chapter-diff-op {
font-size: 10px;
color: var(--fg3);
}
.chapter-diff-block {
font-size: 11px;
font-family: var(--mono);
border-radius: 4px;
overflow: hidden;
margin: 2px 0 6px 0;
border: 1px solid var(--border);
line-height: 1.5;
}
.chapter-diff-del {
background: var(--red-dim);
color: var(--red);
padding: 4px 8px;
white-space: pre-wrap;
word-break: break-all;
border-left: 3px solid var(--red);
max-height: 60px;
overflow: hidden;
position: relative;
}
.chapter-diff-add {
background: var(--green-dim);
color: var(--green);
padding: 4px 8px;
white-space: pre-wrap;
word-break: break-all;
border-left: 3px solid var(--green);
max-height: 60px;
overflow: hidden;
position: relative;
}
.waveform-wrap {
margin-bottom: 12px;
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px 12px;
position: relative;
}
.waveform-label {
font-size: 11px;
color: var(--fg3);
margin-bottom: 8px;
font-weight: 500;
}
.waveform-canvas {
width: 100%;
border-radius: 6px;
cursor: pointer;
display: block;
}
.waveform-tooltip {
position: absolute;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
font-size: 12px;
color: var(--fg);
pointer-events: none;
z-index: 20;
white-space: nowrap;
display: none;
line-height: 1.5;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.waveform-legend {
display: flex;
gap: 14px;
margin-top: 8px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--fg3);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.waveform-axis {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--fg3);
margin-top: 4px;
}
html { scroll-behavior: smooth; }
.waveform-cursor {
position: absolute;
width: 1px;
background: rgba(255,255,255,0.25);
pointer-events: none;
z-index: 10;
display: none;
}
.chapter.highlight {
animation: ch-flash 1.5s ease-out;
}
@keyframes ch-flash {
0% { background: rgba(139, 124, 246, 0.15); }
100% { background: var(--surface); }
}
.chapter.wf-hover {
background: var(--surface2);
transition: background 0.1s;
}
.filter-bar {
background: var(--surface);
border-radius: var(--radius);
padding: 12px 16px;
margin-bottom: 12px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.filter-tools {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.filter-chip {
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
font-family: var(--mono);
background: rgba(255, 255, 255, 0.04);
color: var(--fg3);
cursor: pointer;
user-select: none;
transition: background 0.12s, color 0.12s;
border: 1px solid transparent;
}
.filter-chip:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--fg2);
}
.filter-chip.active {
background: rgba(139, 124, 246, 0.15);
color: var(--accent);
border-color: rgba(139, 124, 246, 0.3);
}
.filter-search {
flex: 1;
min-width: 140px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 10px;
font-size: 12px;
font-family: var(--mono);
color: var(--fg);
outline: none;
transition: border-color 0.15s;
}
.filter-search::placeholder { color: var(--fg3); }
.filter-search:focus { border-color: rgba(139, 124, 246, 0.4); }
.filter-count {
font-size: 11px;
color: var(--fg3);
font-family: var(--mono);
white-space: nowrap;
}
.chapter.filter-hidden { display: none; }
.chapter.kb-focused {
outline: none;
border-left: 2px solid rgba(139, 124, 246, 0.5);
background: rgba(139, 124, 246, 0.04);
}
.chapter.kb-focused.expanded {
border-left: 2px solid rgba(139, 124, 246, 0.5);
background: var(--surface2);
}
.error-summary {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
border-left: 3px solid var(--red);
}
.error-summary-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.error-summary-title {
font-size: 12px;
font-weight: 600;
color: var(--red);
letter-spacing: 0.2px;
}
.error-summary-count {
font-size: 11px;
font-family: var(--mono);
color: var(--fg3);
}
.error-summary-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: flex-start;
}
.error-tools-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.error-tool-chip {
font-size: 11px;
font-family: var(--mono);
padding: 2px 8px;
border-radius: 5px;
background: var(--red-dim);
color: var(--red);
font-weight: 500;
}
.error-streaks {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.error-streak-badge {
font-size: 11px;
font-family: var(--mono);
padding: 2px 8px;
border-radius: 5px;
background: var(--orange-dim);
color: var(--orange);
cursor: pointer;
transition: background 0.12s;
}
.error-streak-badge:hover {
background: rgba(232, 164, 76, 0.2);
}
.error-timeline {
display: flex;
gap: 2px;
align-items: center;
margin-top: 8px;
}
.error-dot {
width: 8px;
height: 8px;
border-radius: 50%;
cursor: pointer;
transition: transform 0.12s, box-shadow 0.12s;
flex-shrink: 0;
}
.error-dot:hover {
transform: scale(1.5);
box-shadow: 0 0 6px var(--red);
}
.error-dot.has-error {
background: var(--red);
}
.error-dot.no-error {
background: rgba(255, 255, 255, 0.06);
}
.error-dot.streak {
background: var(--orange);
box-shadow: 0 0 4px rgba(232, 164, 76, 0.4);
}
.chapter.error-streak-hl {
border-left: 2px solid var(--orange);
}
/* Tool flow visualization */
.tool-flow {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
}
.tool-flow-header {
font-size: 12px;
font-weight: 600;
color: var(--fg2);
margin-bottom: 12px;
letter-spacing: 0.2px;
}
.tool-flow-sequence {
display: flex;
flex-wrap: wrap;
gap: 1px;
margin-bottom: 14px;
border-radius: 4px;
overflow: hidden;
}
.tool-flow-cell {
width: 6px;
height: 18px;
flex-shrink: 0;
opacity: 0.85;
transition: opacity 0.1s, transform 0.1s;
cursor: default;
}
.tool-flow-cell:hover {
opacity: 1;
transform: scaleY(1.4);
z-index: 2;
position: relative;
}
.tool-flow-divider {
width: 2px;
height: 18px;
flex-shrink: 0;
background: var(--border);
margin: 0 1px;
}
.tool-flow-summary {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
margin-bottom: 12px;
}
.tool-flow-stat {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
}
.tool-flow-stat-dot {
width: 10px;
height: 10px;
border-radius: 3px;
flex-shrink: 0;
}
.tool-flow-stat-name {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
flex: 1;
}
.tool-flow-stat-count {
font-size: 12px;
font-family: var(--mono);
color: var(--fg);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.tool-flow-stat-avg {
font-size: 10px;
color: var(--fg3);
font-family: var(--mono);
}
.tool-flow-transitions {
margin-top: 12px;
border-top: 1px solid var(--border);
padding-top: 12px;
}
.tool-flow-transitions-label {
font-size: 11px;
color: var(--fg3);
font-weight: 500;
margin-bottom: 8px;
}
.tool-flow-transition-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tool-flow-transition {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-family: var(--mono);
color: var(--fg2);
padding: 3px 8px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
}
.tool-flow-transition-arrow {
color: var(--fg3);
font-size: 10px;
}
.tool-flow-transition-count {
color: var(--fg3);
font-size: 10px;
margin-left: 2px;
}
/* Tool performance metrics */
.tool-perf {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
}
.tool-perf-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.tool-perf-title {
font-size: 12px;
font-weight: 600;
color: var(--fg2);
letter-spacing: 0.2px;
}
.tool-perf-toggle {
font-size: 11px;
color: var(--fg3);
font-family: var(--mono);
}
.tool-perf-body {
display: none;
margin-top: 14px;
}
.tool-perf.expanded .tool-perf-body { display: block; }
.tool-perf-sort {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.tool-perf-sort-btn {
font-size: 10px;
font-family: var(--mono);
color: var(--fg3);
background: none;
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.tool-perf-sort-btn:hover { color: var(--fg2); border-color: var(--fg3); }
.tool-perf-sort-btn.active { color: var(--accent); border-color: var(--accent); }
.tool-perf-table {
display: flex;
flex-direction: column;
gap: 6px;
}
.tool-perf-row {
display: grid;
grid-template-columns: 10px 90px 50px 1fr 50px 50px;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: background 0.15s;
}
.tool-perf-row:hover { background: rgba(255, 255, 255, 0.05); }
.tool-perf-row.active { background: rgba(139, 124, 246, 0.08); outline: 1px solid rgba(139, 124, 246, 0.3); }
.tool-perf-dot {
width: 10px;
height: 10px;
border-radius: 3px;
flex-shrink: 0;
}
.tool-perf-name {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tool-perf-calls {
font-size: 12px;
font-family: var(--mono);
color: var(--fg);
font-weight: 600;
text-align: right;
font-variant-numeric: tabular-nums;
}
.tool-perf-bar-wrap {
height: 14px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
display: flex;
}
.tool-perf-bar-ok {
height: 100%;
background: var(--green);
opacity: 0.7;
transition: width 0.3s;
}
.tool-perf-bar-err {
height: 100%;
background: var(--red);
opacity: 0.7;
transition: width 0.3s;
}
.tool-perf-rate {
font-size: 11px;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
text-align: right;
}
.tool-perf-rate.perfect { color: var(--green); }
.tool-perf-rate.good { color: var(--fg2); }
.tool-perf-rate.warn { color: var(--orange); }
.tool-perf-rate.bad { color: var(--red); }
.tool-perf-errs {
font-size: 11px;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
color: var(--fg3);
text-align: right;
}
.tool-perf-errs.has-errors { color: var(--red); }
.tool-perf-retries {
font-size: 10px;
font-family: var(--mono);
color: var(--fg3);
margin-top: 2px;
grid-column: 3 / -1;
}
/* Cost chart */
.cost-chart {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px 12px;
margin-bottom: 12px;
position: relative;
}
.cost-chart-header {
font-size: 12px;
font-weight: 600;
color: var(--fg2);
margin-bottom: 4px;
letter-spacing: 0.2px;
display: flex;
align-items: baseline;
gap: 10px;
}
.cost-chart-total {
font-size: 11px;
color: var(--orange);
font-weight: 500;
}
.cost-chart-canvas {
width: 100%;
height: 100px;
border-radius: 6px;
cursor: crosshair;
display: block;
}
.cost-chart-tooltip {
position: absolute;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 10px;
font-size: 11px;
color: var(--fg);
pointer-events: none;
z-index: 20;
white-space: nowrap;
display: none;
line-height: 1.4;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.cost-chart-axis {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--fg3);
margin-top: 4px;
}
/* File hotspot visualization */
.file-hotspot {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
}
.file-hotspot-header {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.file-hotspot-title {
font-size: 12px;
font-weight: 600;
color: var(--fg2);
letter-spacing: 0.2px;
}
.file-hotspot-toggle {
font-size: 10px;
color: var(--fg3);
font-family: var(--mono);
}
.file-hotspot-count {
font-size: 11px;
color: var(--fg3);
font-family: var(--mono);
margin-left: auto;
}
.file-hotspot-body {
margin-top: 12px;
display: none;
}
.file-hotspot.expanded .file-hotspot-body {
display: block;
}
.file-hotspot-list {
display: flex;
flex-direction: column;
gap: 3px;
max-height: 360px;
overflow-y: auto;
}
.file-hotspot-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
border: 1px solid transparent;
}
.file-hotspot-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.file-hotspot-item.active {
background: rgba(139, 124, 246, 0.08);
border-color: rgba(139, 124, 246, 0.25);
}
.file-hotspot-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
transition: width 0.15s, height 0.15s;
}
.file-hotspot-path {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-hotspot-ops {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.file-hotspot-op {
font-size: 10px;
font-family: var(--mono);
padding: 1px 5px;
border-radius: 3px;
font-weight: 500;
}
.file-hotspot-op.read { background: rgba(107, 164, 232, 0.12); color: #6ba4e8; }
.file-hotspot-op.edit { background: rgba(224, 196, 94, 0.12); color: #e0c45e; }
.file-hotspot-op.write { background: rgba(216, 150, 96, 0.12); color: #d89660; }
.file-hotspot-touch {
font-size: 11px;
font-family: var(--mono);
color: var(--fg3);
font-variant-numeric: tabular-nums;
min-width: 24px;
text-align: right;
flex-shrink: 0;
}
.file-hotspot-chapters {
font-size: 10px;
color: var(--fg3);
font-family: var(--mono);
flex-shrink: 0;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-hotspot-bar {
height: 3px;
border-radius: 2px;
margin-top: 8px;
display: flex;
gap: 1px;
overflow: hidden;
}
.file-hotspot-bar-seg {
height: 100%;
flex-shrink: 0;
}
/* Git timeline panel */
.git-timeline {
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
border-left: 3px solid var(--orange);
}
.git-timeline-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.git-timeline-title {
font-size: 12px;
font-weight: 600;
color: var(--orange);
letter-spacing: 0.2px;
}
.git-timeline-count {
font-size: 11px;
font-family: var(--mono);
color: var(--fg3);
}
.git-timeline-ops {
display: flex;
flex-direction: column;
gap: 6px;
}
.git-op {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
}
.git-op-icon {
font-size: 13px;
flex-shrink: 0;
width: 18px;
text-align: center;
}
.git-op-icon.commit { color: var(--green); }
.git-op-icon.branch { color: var(--accent); }
.git-op-icon.push { color: var(--orange); }
.git-op-icon.merge { color: #c88abd; }
.git-op-icon.other { color: var(--fg3); }
.git-op-detail {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.git-op-hash {
color: var(--orange);
font-size: 11px;
margin-right: 6px;
}
.git-op-msg {
color: var(--fg);
font-size: 12px;
}
.git-op-branch-name {
color: var(--accent);
font-weight: 500;
}
.git-op-chapter {
font-size: 10px;
color: var(--fg3);
flex-shrink: 0;
cursor: pointer;
}
.git-op-chapter:hover {
color: var(--accent);
}
/* Git badge in chapter header */
.chapter-git-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-family: var(--mono);
padding: 1px 6px;
border-radius: 4px;
background: var(--orange-dim);
color: var(--orange);
font-weight: 500;
}
/* Git section in chapter detail */
.chapter-git-ops { margin-bottom: 10px; }
.chapter-git-op {
font-size: 12px;
font-family: var(--mono);
color: var(--fg2);
padding: 3px 0;
display: flex;
gap: 6px;
align-items: baseline;
}
.chapter-git-op-icon { font-size: 11px; }
.chapter-git-op-icon.commit { color: var(--green); }
.chapter-git-op-icon.branch { color: var(--accent); }
.chapter-git-op-icon.push { color: var(--orange); }
.chapter-git-op-icon.merge { color: #c88abd; }
.session-summary {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-bottom: 12px;
background: var(--surface);
border-radius: var(--radius);
padding: 14px 20px;
align-items: center;
}
.session-summary-item {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
color: var(--fg2);
}
.session-summary-item .ss-label {
font-size: 11px;
color: var(--fg3);
font-weight: 500;
}
.session-summary-item .ss-value {
font-weight: 600;
color: var(--fg);
}
.session-summary-item.ss-cost .ss-value { color: var(--orange); }
.session-summary-item.ss-errors .ss-value { color: var(--red); }
.session-summary-item.ss-commits .ss-value { color: var(--green); }
.session-summary-item.ss-duration .ss-value { color: var(--accent); }
.session-summary-item.ss-quality-mixed .ss-value { color: var(--orange); }
.session-summary-item.ss-waste .ss-value { color: var(--orange); }
.session-summary-sep {
width: 1px;
height: 18px;
background: var(--border);
flex-shrink: 0;
}
.session-grade {
display: flex;
align-items: center;
gap: 14px;
margin-right: 6px;
}
.session-grade-letter {
font-family: var(--mono);
font-size: 28px;
font-weight: 700;
line-height: 1;
min-width: 32px;
text-align: center;
}
.session-grade-letter.grade-a { color: var(--green); }
.session-grade-letter.grade-b { color: var(--green); opacity: 0.8; }
.session-grade-letter.grade-c { color: var(--orange); }
.session-grade-letter.grade-d { color: #d97740; }
.session-grade-letter.grade-f { color: var(--red); }
.session-grade-detail {
display: flex;
flex-direction: column;
gap: 2px;
}
.session-grade-score {
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
font-variant-numeric: tabular-nums;
}
.session-grade-breakdown {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
}
.session-grade-factor {
font-family: var(--mono);
font-size: 10px;
color: var(--fg3);
display: inline-flex;
align-items: center;
gap: 3px;
}
.session-grade-factor-bar {
display: inline-block;
width: 28px;
height: 4px;
border-radius: 2px;
background: var(--border);
position: relative;
overflow: hidden;
}
.session-grade-factor-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
border-radius: 2px;
}
.session-grade-factor-fill.fill-good { background: var(--green); }
.session-grade-factor-fill.fill-ok { background: var(--orange); }
.session-grade-factor-fill.fill-bad { background: var(--red); }
.session-grade-note {
font-size: 11px;
color: var(--fg3);
font-style: italic;
margin-top: 1px;
}
.activity-timeline {
margin-bottom: 12px;
background: var(--surface);
border-radius: var(--radius);
padding: 16px 20px 12px;
position: relative;
z-index: 10;
}
.activity-timeline-label {
font-size: 11px;
color: var(--fg3);
margin-bottom: 8px;
font-weight: 500;
}
.activity-timeline-canvas {
width: 100%;
border-radius: 6px;
display: block;
cursor: crosshair;
}
.activity-timeline-times {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 10px;
font-family: var(--mono);
color: var(--fg3);
}
.activity-timeline-legend {
display: flex;
gap: 14px;
margin-top: 8px;
font-size: 10px;
color: var(--fg3);
}
.activity-timeline-legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.activity-timeline-legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.activity-timeline-tooltip {
position: absolute;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 10px;
font-size: 11px;
color: var(--fg);
pointer-events: none;
z-index: 20;
white-space: nowrap;
display: none;
line-height: 1.5;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
/* Chapter hover tooltip */
.chapter-tooltip {
position: fixed;
z-index: 9999;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
min-width: 220px;
max-width: 340px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
font-size: 12px;
line-height: 1.5;
}
.chapter-tooltip.visible { opacity: 1; }
.chapter-tooltip-prompt {
color: var(--fg);
font-weight: 500;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 310px;
}
.chapter-tooltip-tools {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.chapter-tooltip-tool {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.chapter-tooltip-row {
display: flex;
align-items: center;
gap: 6px;
color: var(--fg2);
margin-bottom: 3px;
}
.chapter-tooltip-row:last-child { margin-bottom: 0; }
.chapter-tooltip-label {
color: var(--fg3);
min-width: 48px;
}
.chapter-tooltip-value {
color: var(--fg);
font-family: var(--mono);
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chapter-tooltip-value.errors { color: var(--red); }
.chapter-tooltip-value.clean { color: var(--green); }
.chapter-tooltip-value.corrected { color: var(--orange); }
.chapter-tooltip-value.struggling { color: var(--red); }
.chapter-tooltip-files {
color: var(--fg2);
font-family: var(--mono);
font-size: 11px;
margin-top: 2px;
}
.chapter-tooltip-file {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
/* Session mini-map / navigation sidebar */
.minimap {
position: fixed;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 28px;
z-index: 500;
display: flex;
flex-direction: column;
gap: 1px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 3px;
max-height: 80vh;
overflow: hidden;
opacity: 0.6;
transition: opacity 0.2s;
}
.minimap:hover { opacity: 1; }
.minimap-block {
width: 22px;
border-radius: 2px;
cursor: pointer;
position: relative;
transition: outline 0.15s;
outline: 1.5px solid transparent;
flex-shrink: 0;
}
.minimap-block:hover { outline-color: var(--fg3); }
.minimap-block.mm-clean { background: var(--green); opacity: 0.45; }
.minimap-block.mm-corrected { background: var(--orange); opacity: 0.6; }
.minimap-block.mm-struggling { background: var(--red); opacity: 0.65; }
.minimap-block.mm-visible {
outline-color: var(--accent);
outline-width: 2px;
opacity: 1;
}
.minimap-marker {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
right: -1px;
}
.minimap-marker.mm-error { background: var(--red); top: 1px; }
.minimap-marker.mm-commit { background: var(--green); bottom: 1px; }
.minimap-tooltip {
position: fixed;
pointer-events: none;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
font-size: 11px;
font-family: var(--mono);
color: var(--fg);
white-space: nowrap;
z-index: 9999;
opacity: 0;
transition: opacity 0.12s;
}
.minimap-tooltip.visible { opacity: 1; }
.minimap-viewport {
position: absolute;
left: 0;
width: 100%;
border: 1.5px solid var(--accent);
border-radius: 3px;
background: rgba(139, 124, 246, 0.08);
pointer-events: none;
transition: top 0.1s ease-out, height 0.1s ease-out;
z-index: 1;
}
/* Chapter dependency / relationship indicators */
.chapter-dep-badge {
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
font-family: var(--mono);
background: rgba(139, 124, 246, 0.1);
color: var(--accent);
cursor: pointer;
transition: background 0.12s;
}
.chapter-dep-badge:hover {
background: rgba(139, 124, 246, 0.2);
}
.chapter-dep-badge.dep-fix {
background: rgba(74, 222, 128, 0.1);
color: var(--green);
}
.chapter-dep-badge.dep-fix:hover {
background: rgba(74, 222, 128, 0.18);
}
.chapter-dep-connector {
position: absolute;
left: 27px;
width: 2px;
background: rgba(139, 124, 246, 0.15);
z-index: 0;
pointer-events: none;
}
.chapter { position: relative; }
.chapter-dep-section {
padding: 8px 14px;
margin-bottom: 8px;
border-left: 2px solid rgba(139, 124, 246, 0.25);
background: rgba(139, 124, 246, 0.03);
border-radius: 0 6px 6px 0;
font-size: 12px;
color: var(--fg2);
}
.chapter-dep-section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--fg3);
margin-bottom: 4px;
}
.chapter-dep-link {
display: inline-block;
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
cursor: pointer;
margin-right: 8px;
padding: 1px 5px;
border-radius: 3px;
transition: background 0.12s;
}
.chapter-dep-link:hover {
background: rgba(139, 124, 246, 0.12);
}
.chapter-dep-shared-files {
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
margin-left: 4px;
}
@media (max-width: 768px) {
#app { padding: 16px 12px; }
.stats-bar { flex-wrap: wrap; }
.stat { min-width: 33%; border-right: none; padding: 8px 12px; }
.filter-bar { flex-direction: column; align-items: stretch; }
.filter-search { min-width: 100%; }
.tool-flow-summary { grid-template-columns: 1fr; }
.tool-flow-cell { width: 4px; height: 14px; }
.tool-perf-row { grid-template-columns: 10px 70px 40px 1fr 40px 40px; gap: 4px; padding: 4px 6px; }
.tool-perf-name { font-size: 11px; }
.tool-perf-retries { display: none; }
.file-hotspot-ops { display: none; }
.file-hotspot-chapters { display: none; }
.session-summary { gap: 4px 12px; }
.session-summary-sep { display: none; }
.session-grade-letter { font-size: 22px; }
.session-grade-breakdown { display: none; }
.activity-timeline-legend { flex-wrap: wrap; gap: 8px; }
.activity-timeline-times { font-size: 9px; }
.chapter-tooltip { display: none; }
.minimap { display: none; }
.minimap-tooltip { display: none; }
}
/* Print / PDF-friendly stylesheet */
@media print {
:root {
--bg: #ffffff;
--surface: #ffffff;
--surface2: #f5f5f7;
--fg: #111113;
--fg2: #444449;
--fg3: #6b6b73;
--border: rgba(0, 0, 0, 0.1);
--accent: #6d5cce;
--green: #1a8a42;
--green-dim: rgba(26, 138, 66, 0.08);
--red: #c43838;
--red-dim: rgba(196, 56, 56, 0.08);
--orange: #b57820;
--orange-dim: rgba(181, 120, 32, 0.08);
}
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body {
background: #fff;
color: #111;
font-size: 11px;
line-height: 1.5;
}
#app {
max-width: 100%;
padding: 0;
margin: 0;
}
@page {
margin: 1.5cm 1.2cm;
size: auto;
}
/* Remove interactive / decorative elements */
.filter-bar,
.filter-chip,
.filter-search,
.filter-count,
.expand-toggle,
.chapter-tooltip,
.chapter-permalink,
.export-btn,
.print-btn,
.md-btn,
.hf-btn,
.hf-modal-overlay,
.waveform-tooltip,
.waveform-cursor,
.cost-chart-tooltip,
.activity-timeline-tooltip,
.error-streak-badge,
.error-dot,
.minimap,
.minimap-tooltip { display: none !important; }
/* Remove hover/transition effects */
.chapter { transition: none; }
.chapter:hover { background: var(--surface); }
/* Expand all chapters — show all detail */
.chapter-detail { display: block !important; }
.chapter-prompt-text {
display: block !important;
-webkit-line-clamp: unset !important;
}
.chapter.filter-hidden { display: block !important; }
/* Remove max-height constraints on all content blocks */
.chapter-output,
.chapter-diff-del,
.chapter-diff-add,
.chapter-agent-prompt,
.chapter-thinking-block,
.chapter-mcp-output {
max-height: none !important;
overflow: visible !important;
}
/* Keep canvas visualizations visible */
.waveform-canvas,
.cost-chart-canvas,
.activity-timeline-canvas {
max-width: 100%;
}
/* Panel backgrounds — light borders instead of colored surfaces */
.header,
.stats-bar,
.session-summary,
.activity-timeline,
.error-summary,
.waveform-wrap,
.tool-flow,
.tool-perf,
.cost-chart,
.file-hotspot,
.git-timeline,
.chapter {
background: #fff;
border: 1px solid #ddd;
box-shadow: none;
}
/* Avoid page breaks inside chapters */
.chapter {
break-inside: avoid;
page-break-inside: avoid;
}
/* Also avoid breaks inside key panels */
.session-summary,
.stats-bar,
.git-timeline,
.error-summary {
break-inside: avoid;
page-break-inside: avoid;
}
/* Reduce spacing for print density */
.chapters { gap: 4px; }
.header { padding: 16px 20px; margin-bottom: 8px; }
.stats-bar { margin-bottom: 8px; }
.session-summary { margin-bottom: 8px; }
/* Sensible font sizes */
.stat-value { font-size: 18px; }
.stat-label { font-size: 10px; }
.chapter-prompt { font-size: 12px; }
.chapter-response { font-size: 11px; }
.chapter-cmd { font-size: 11px; }
.chapter-output { font-size: 10px; }
.chapter-file { font-size: 11px; }
/* Remove decorative colored left borders — use thin gray */
.error-summary { border-left-color: #aaa; }
.git-timeline { border-left-color: #aaa; }
.chapter-quality { border-left-color: #aaa; }
.chapter-efficiency { border-left-color: #aaa; }
.chapter-thinking { border-left-color: #aaa; }
/* Tool performance — expand body for print */
.tool-perf-body { display: block !important; }
.tool-perf-toggle { display: none; }
.tool-perf-sort { display: none; }
.tool-perf-row { cursor: default; }
/* File hotspot — expand body for print */
.file-hotspot-body { display: block !important; }
.file-hotspot-list { max-height: none !important; overflow: visible !important; }
.file-hotspot-toggle { display: none; }
/* Keyboard focus indicator — not relevant in print */
.chapter.kb-focused { border-left: none; background: #fff; }
/* Links — show URL for context */
a[href] { color: var(--accent); text-decoration: underline; }
/* Tool flow cells — slightly larger for print legibility */
.tool-flow-cell { width: 8px; height: 20px; }
}
</style>
</head>
<body>
<div id="app"></div>
<script>
const SESSION = {"sessionId":"019efdd4-fc58-7a20-baf4-5ffed8e5f99d","source":"grok","cwd":"/home/everlier/code/tracequest","model":"grok-composer-2.5-fast","gitBranch":null,"startTime":"2026-06-25T08:11:04.224Z","endTime":"2026-06-25T08:11:15.505Z","durationMs":11281,"eventCount":29,"events":[{"type":"user","timestamp":"2026-06-25T08:11:04.224Z","text":"Explain me the tracequest share feature","uuid":"grok-user-0"},{"type":"tool_result","timestamp":"2026-06-25T08:11:06.504Z","toolUseId":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_Y3P6k","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsidecar.facts\n.facts\ntests/integration.md\nREADME.md\ntests/sharing-integration.md\ntests/index-search-integration.md\ntests/large-session-integration.md\ntests/edge-integration.md\ntests/opencode-integration.md\ntests/serve-integration.md\ntests/agent-history-integration.md\ntest/share/fetch-retry.test.js\ntests/multi-source-integration.md\ntest/share/pipeline.test.js\ntests/README.md\ntest/dead-code-sweep.test.js\ntest/share/share-bundle.test.js\ntest/share/scanner.test.js\ntest/render-css.test.js\ntest/route-handlers-api.test.js\ntest/share/adapters.test.js\ntest/render-chapters.test.js\ntest/share/cli-share.test.js\ntest/share/metadata.test.js\ntest/chapter-patterns.test.js\ntest/filter-expr.test.js\nsidecar/src/indexer.rs\ntest/index-writers.test.js\ntest/sidecar-incremental-invalidation-parity.test.js\ntest/render-ui-chapters.test.js\nbin/tracequest.js\nsidecar/src/indexer_tests.rs\ntest/sessions.test.js\ntest/server/server-helpers-stats.test.j","isError":false,"uuid":"grok-result-1"},{"type":"tool_result","timestamp":"2026-06-25T08:11:06.520Z","toolUseId":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_wh5HK","text":"Result of search in '' (total 8 files):\n- ./src/share/share-bundle.js\n- ./test/share/share-bundle.test.js\n- ./test/browser/share-modal-hf.playwright.test.js\n- ./test/browser/share-modal.playwright.test.js\n- ./test/share/cli-share.test.js\n- ./src/render/render-share.js\n- ./test/browser/shared-css-tokens.test.js\n- ./src/browser/shared-css-tokens.js\n","isError":false,"uuid":"grok-result-2"},{"type":"tool_result","timestamp":"2026-06-25T08:11:06.537Z","toolUseId":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_ADQJ9","text":"Exit code: 0\n\nCommand output:\n\n```\nk8o ✓ share > share > flow > the share pipeline is: parse session → scan for secrets → redact findings in a session copy → render HTML from the clean copy → upload; secrets never leave the local machine @share @spec @implemented\ncof ✓ share > share > flow > redaction replaces each matched secret in every string-bearing field of the redacted session copy with [REDACTED] — including event.text, toolCalls[].input, thinking blocks, diffInfo, mcpInfo, and agentHistory records that renderHTML embeds; the redacted session is a deep copy — the original session data is never mutated @share @spec @implemented\nb8p ✓ share > share > flow > the share command is a new CLI subcommand alongside render, list, latest, find, search, messages, serve @share @spec @implemented\nio7 ? share > share > flow > both GitHub Gist and Hugging Face Hub APIs support browser-origin requests via CORS (GitHub returns access-control-allow-origin: *; HF reflects the request Ori","isError":false,"uuid":"grok-result-3"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"I'll search the codebase and fact sheet for how the share feature is implemented and documented.\n","toolCalls":[{"id":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_Y3P6k","name":"Grep","input":"share "},{"id":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_ADQJ9","name":"Bash","input":"facts list --section \"share\" 2>/dev/null || facts list 2>/dev/null | rg -i share | head -40"},{"id":"call-f796ee2c-05e8-49eb-8a48-d85c77abc194-composer_call_wh5HK","name":"Glob","input":""}],"tokens":{"input":3934,"output":67,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-4"},{"type":"tool_result","timestamp":"2026-06-25T08:11:08.506Z","toolUseId":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_by04n","text":"# Sharing Integration Tests\n\nTests the share pipeline (CLI and browser UI) defined in `.facts` under the `# share` section. The feature is currently spec'd as `@draft`; these tests verify whether the implementation meets the spec.\n\n## Integration test principles\n\n1. **Real workflows** — invoke `bin/tracequest.js` end-to-end (parse → scan → redact → render → upload), not isolated unit mocks of internal helpers.\n2. **Verifiable expectations** — each test states observable pass/fail criteria (exit codes, JSON fields, HTML script markers, mock-server request counts) before execution.\n3. **Isolated fixtures** — synthetic sessions under temp dirs; `HOME` override for discovery tests; mock Gist/HF servers via `GITHUB_API_URL` / `HF_API_URL` so uploads never hit production APIs.\n 10|4. **Stdout/stderr contract** — `--json` stdout must be parseable JSON only; progress/diagnostics (parsing, scanning, index build) belong on stderr.\n\nAutomated harness: `test/bin/tracequest-sharing-integration.t","isError":false,"uuid":"grok-result-5"},{"type":"tool_result","timestamp":"2026-06-25T08:11:08.519Z","toolUseId":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_MimB9","text":"Result of search in '' (total 10 files):\n- ./src/share/scanner.js\n- ./src/share/share-bundle.js\n- ./src/share/hf-adapter.js\n- ./src/share/gist-adapter.js\n- ./src/share/fetch-retry.js\n- ./src/share/pipeline.js\n- ./src/share/metadata.js\n- ./src/share/redactor.js\n- ./src/share/auth.js\n- ./src/share/secret-rules.js\n","isError":false,"uuid":"grok-result-6"},{"type":"tool_result","timestamp":"2026-06-25T08:11:08.530Z","toolUseId":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_N4b9D","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsrc/cli/cli-die.js\nsrc/cli/cli-commands.js\n<\/workspace_result>","isError":false,"uuid":"grok-result-7"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"","toolCalls":[{"id":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_MimB9","name":"Glob","input":""},{"id":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_N4b9D","name":"Grep","input":"cmdShare|share "},{"id":"call-fe7c6a62-8d27-44e6-bc5a-c07ac26f8ebb-composer_call_by04n","name":"Read","input":"/home/everlier/code/tracequest/tests/sharing-integration.md"}],"tokens":{"input":3934,"output":37,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-8"},{"type":"tool_result","timestamp":"2026-06-25T08:11:10.072Z","toolUseId":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_N4b9D","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsrc/cli/cli-commands.js\n 487:export async function cmdShare(positionals, values) {\n 488- if (values.help) showShareHelp();\n 489-\n 490- const target = values.target || \"gist\";\n<\/workspace_result>","isError":false,"uuid":"grok-result-9"},{"type":"tool_result","timestamp":"2026-06-25T08:11:10.090Z","toolUseId":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_fyBcC","text":"import { readSync } from \"node:fs\";\nimport { parseSession } from \"../parse.js\";\nimport { renderHTML } from \"../render.js\";\nimport { loadSecretRules } from \"./secret-rules.js\";\nimport { scanSessionForSecrets } from \"./scanner.js\";\nimport { redactSession } from \"./redactor.js\";\nimport { buildShareMetadata, publicFindings } from \"./metadata.js\";\nimport { shareGist } from \"./gist-adapter.js\";\nimport { shareHf, buildHfSidecar } from \"./hf-adapter.js\";\n 10|import { resolveGhToken, resolveHfToken } from \"./auth.js\";\n\n/**\n * Run the share pipeline: scan → redact → render → upload.\n * Secrets never leave the machine in plaintext.\n */\nexport async function runSharePipeline({\n sessionPath,\n session: parsedSession,\n findings: preFindings = null,\n 20| discovery = null,\n target = \"gist\",\n isPrivate = false,\n hfRepo = null,\n token = null,\n fetchImpl = fetch,\n}) {\n const session = parsedSession || parseSession(sessionPath);\n const rules = loadSecretRules();\n const findings = preFindi","isError":false,"uuid":"grok-result-10"},{"type":"tool_result","timestamp":"2026-06-25T08:11:10.108Z","toolUseId":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_by04n","text":"import { buildSessionChapters } from \"../chapters/session-chapters.js\";\nimport { enrichChaptersForRender } from \"../chapters/chapter-render-enrich.js\";\nimport { chapterFileKeys } from \"../chapters/chapter-keys.js\";\nimport { FIRST_PROMPT_MAX_LEN, safeSlice, sumToolCounts } from \"../parse/parse-utils.js\";\nimport { formatDuration, estimateParsedStatsCost } from \"../filter/filter-formats.js\";\nimport { normalizeProjectFolder } from \"../server/server-session-path.js\";\nimport { buildIndex, peekSession } from \"../sessions.js\";\nimport { getSessionMeta } from \"../server/server-helpers.js\";\n\n 10|function aggregateChapterMetrics(chapters) {\n let totalErrors = 0;\n let totalRetries = 0;\n let totalSelfCorrections = 0;\n let cleanCount = 0;\n let correctedCount = 0;\n for (const ch of chapters) {\n totalErrors += ch.errors || 0;\n totalRetries += ch.retries || 0;\n totalSelfCorrections += ch.selfCorrections || 0;\n 20| if (ch.outcome === \"clean\") cleanCount++;\n else if (ch.outcome ","isError":false,"uuid":"grok-result-11"},{"type":"tool_result","timestamp":"2026-06-25T08:11:10.126Z","toolUseId":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_SXlHb","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsrc/render/render-css.js\nsrc/render/render-share.js\nsrc/render/render-assemble.js\nsrc/render/render-core-shell.js\nsrc/render/render-session-css.js\nsrc/render/render-ui-detail.js\nsrc/render/render-session-rules.js\n<\/workspace_result>","isError":false,"uuid":"grok-result-12"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"","toolCalls":[{"id":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_fyBcC","name":"Read","input":"/home/everlier/code/tracequest/src/share/pipeline.js"},{"id":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_N4b9D","name":"Grep","input":"cmdShare|function.*share "},{"id":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_by04n","name":"Read","input":"/home/everlier/code/tracequest/src/share/metadata.js"},{"id":"call-fbfe1288-8eac-4735-a692-63022479fb27-composer_call_SXlHb","name":"Grep","input":"share|Share "}],"tokens":{"input":3934,"output":59,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-13"},{"type":"tool_result","timestamp":"2026-06-25T08:11:11.980Z","toolUseId":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_2f3Y7","text":"import { shareFetch } from \"./fetch-retry.js\";\n\nconst DEFAULT_HF_API_URL = \"https://huggingface.co\";\n/** HF inline commit size limit; larger files use preupload. */\nexport const HF_PREUPLOAD_THRESHOLD = 1_048_576;\n\nexport function hfApiUrl() {\n return (process.env.HF_API_URL || DEFAULT_HF_API_URL).replace(/\\/$/, \"\");\n}\n 10|\nconst README_CARD = `---\nlicense: mit\ntags:\n- agent-traces\n- tracequest\ntask_categories:\n- text-generation\n---\n\n 20|# tracequest sessions\n\nAgent session traces shared via [tracequest](https://github.com/av/tracequest).\n`;\n\n/** Sanitize a path segment (alphanumeric, hyphen, underscore only). */\nexport function sanitizePathSegment(seg) {\n const s = String(seg || \"unknown\").replace(/[^a-zA-Z0-9_-]/g, \"-\");\n return s || \"unknown\";\n}\n 30|\nfunction parseRepo(repo) {\n const parts = String(repo || \"\").split(\"/\").filter(Boolean);\n if (parts.length !== 2) throw new Error(\"HF repo must be namespace/repo (e.g. user/tracequest-sessions)\");\n return { namespace: par","isError":false,"uuid":"grok-result-14"},{"type":"tool_result","timestamp":"2026-06-25T08:11:11.997Z","toolUseId":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_by04n","text":"import { sanitizePathSegment } from \"./hf-adapter.js\";\nimport { shareFetch } from \"./fetch-retry.js\";\n\nconst DEFAULT_GITHUB_API_URL = \"https://api.github.com\";\nconst GISTHOST_BASE = \"https://gisthost.github.io/?\";\n\nexport function gistApiUrl() {\n return (process.env.GITHUB_API_URL || DEFAULT_GITHUB_API_URL).replace(/\\/$/, \"\");\n}\n 10|\n/** Build a gisthost preview URL for a gist id and optional filename. */\nexport function buildGistHostPreviewUrl(gistId, filename) {\n if (!gistId) return null;\n const base = `${GISTHOST_BASE}${gistId}`;\n if (filename && filename !== \"index.html\") return `${base}/${filename}`;\n return base;\n}\n\n/**\n 20| * Upload rendered HTML to a GitHub Gist.\n * @returns {Promise<{url: string, id: string, previewUrl: string, filename: string}>}\n * `url` — raw gist.github.com HTML page (not gisthost preview)\n * `previewUrl` — gisthost rendered preview URL (primary share link)\n * `filename` — uploaded gist file name\n */\nexport async function shareGist({ html,","isError":false,"uuid":"grok-result-15"},{"type":"tool_result","timestamp":"2026-06-25T08:11:12.014Z","toolUseId":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_fyBcC","text":"... 486 lines not shown ...\nexport async function cmdShare(positionals, values) {\n if (values.help) showShareHelp();\n\n 490| const target = values.target || \"gist\";\n if (target !== \"gist\" && target !== \"hf\") {\n die(`Invalid --target: ${target}\\nUse gist or hf.`);\n }\n\n const { path: sessionPath, discovery } = resolveShareSession(positionals, values);\n\n console.error(k.dim(\"Parsing session...\"));\n let session;\n try {\n 500| session = parseSession(sessionPath);\n } catch (err) {\n die(`Failed to parse session ${sessionPath}: ${err.message}`);\n }\n\n const sidecars = discoverAgentSidecarPaths(sessionPath);\n if (sidecars.length) {\n const agentHistory = loadAgentHistoryForSession(sessionPath, sidecars);\n if (agentHistory) {\n session.agentHistory = agentHistory;\n 510| session.agentSidecarCount = sidecars.length;\n }\n }\n\n const rules = loadSecretRules();\n const findings = scanSessionForSecrets(session, rules);\n if (findings.length) {\n console.error","isError":false,"uuid":"grok-result-16"},{"type":"tool_result","timestamp":"2026-06-25T08:11:12.028Z","toolUseId":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_SBQJo","text":"import { secretRulesJson } from \"../share/secret-rules.js\";\nimport { buildScannerRedactorSrc } from \"../share/share-bundle.js\";\n\nconst SCANNER_REDACTOR_SRC = buildScannerRedactorSrc();\n\nexport const SHARE_JS = `\n const SECRET_RULES = ${secretRulesJson()};\n\n ${SCANNER_REDACTOR_SRC}\n 10|\n const GH_LS_KEY = 'tracequest-gh-token';\n const HF_LS_KEY = 'tracequest-hf-token';\n const HF_REPO_LS_KEY = 'tracequest-hf-repo';\n const HF_PREUPLOAD_THRESHOLD = 1048576;\n\n function sanitizePathSegment(seg) {\n var s = String(seg || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '-');\n return s || 'unknown';\n }\n 20|\n function escHtml(s) {\n return String(s).replace(/&/g, '&amp;').replace(/<\/g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n }\n\n function isAllowedApexHost(hostname, apex) {\n if (hostname === apex) return true;\n var suffix = '.' + apex;\n if (hostname.slice(-suffix.length) !== suffix) return false;\n var sub = hostname.slice(0, -suffix.length);\n 30| re","isError":false,"uuid":"grok-result-17"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"","toolCalls":[{"id":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_fyBcC","name":"Read","input":"/home/everlier/code/tracequest/src/cli/cli-commands.js"},{"id":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_SBQJo","name":"Read","input":"/home/everlier/code/tracequest/src/render/render-share.js"},{"id":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_by04n","name":"Read","input":"/home/everlier/code/tracequest/src/share/gist-adapter.js"},{"id":"call-9d8e02dd-3977-4777-80bb-9cf5366ef309-composer_call_2f3Y7","name":"Read","input":"/home/everlier/code/tracequest/src/share/hf-adapter.js"}],"tokens":{"input":3934,"output":79,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-18"},{"type":"tool_result","timestamp":"2026-06-25T08:11:13.677Z","toolUseId":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_SBQJo","text":"const PEM_BLOCK_RE = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g;\n\nexport function redactString(str, secrets) {\n if (!str || typeof str !== \"string\") return str;\n let out = str;\n for (const secret of secrets) {\n if (!secret || !out.includes(secret)) continue;\n out = out.split(secret).join(\"[REDACTED]\");\n }\n 10| return out;\n}\n\nexport function redactPemBlocks(str) {\n if (!str || typeof str !== \"string\") return str;\n return str.replace(PEM_BLOCK_RE, \"[REDACTED]\");\n}\n\nexport function redactValue(val, secrets) {\n if (typeof val === \"string\") return redactPemBlocks(redactString(val, secrets));\n 20| if (Array.isArray(val)) return val.map((item) => redactValue(item, secrets));\n if (val && typeof val === \"object\") {\n for (const key of Object.keys(val)) {\n val[key] = redactValue(val[key], secrets);\n }\n return val;\n }\n return val;\n}\n\n 30|/**\n * Deep-copy a session and replace matched secrets w","isError":false,"uuid":"grok-result-19"},{"type":"tool_result","timestamp":"2026-06-25T08:11:13.697Z","toolUseId":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_by04n","text":"import { readFileSync, existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/** Resolve GitHub token: GITHUB_TOKEN → gh auth token → error. */\nexport function resolveGhToken() {\n if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN.trim();\n try {\n 10| return execFileSync(\"gh\", [\"auth\", \"token\"], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }).trim();\n } catch {\n return null;\n }\n}\n\n/** Resolve Hugging Face token: HF_TOKEN → ~/.cache/huggingface/token → error. */\nexport function resolveHfToken() {\n if (process.env.HF_TOKEN) return process.env.HF_TOKEN.trim();\n const cachePath = join(homedir(), \".cache\", \"huggingface\", \"token\");\n 20| if (existsSync(cachePath)) {\n try {\n return readFileSync(cachePath, \"utf8\").trim();\n } catch {\n return null;\n }\n }\n return null;\n}\n","isError":false,"uuid":"grok-result-20"},{"type":"tool_result","timestamp":"2026-06-25T08:11:13.716Z","toolUseId":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_fyBcC","text":"/** Shannon entropy for gitleaks-style rule checks. */\nexport function shannonEntropy(str) {\n if (!str || str.length === 0) return 0;\n const freq = new Map();\n for (const c of str) freq.set(c, (freq.get(c) || 0) + 1);\n let entropy = 0;\n const len = str.length;\n for (const count of freq.values()) {\n const p = count / len;\n 10| entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n/** Redact a matched secret for display: first 4 + last 4 chars. */\nexport function redactMatchDisplay(match) {\n if (!match) return \"****\";\n if (match.length <= 8) return \"****\";\n return match.slice(0, 4) + \"…\" + match.slice(-4);\n 20|}\n\n/** Map gitleaks PCRE inline flag letters to JavaScript RegExp flags. */\nconst PCRE_INLINE_FLAG_MAP = {\n i: \"i\",\n m: \"m\",\n s: \"s\",\n u: \"u\",\n};\n\n 30|/** Normalize gitleaks PCRE inline flags (e.g. `(?i)`, `(?is)`) for JavaScript RegExp. */\nexport function normalizeRuleRegex(pattern, flags = \"g\") {\n let source = String(pattern);\n let outFlags = flags.i","isError":false,"uuid":"grok-result-21"},{"type":"tool_result","timestamp":"2026-06-25T08:11:13.733Z","toolUseId":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_SXlHb","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsrc/render/render-core-shell.js\n 112-\n 113: const shareBtn = h('button', { className: 'hf-btn', title: 'Share session to Gist or Hugging Face' });\n 114- shareBtn.appendChild(shareIcon);\n 115: shareBtn.appendChild(document.createTextNode('Share'));\n 116- shareBtn.addEventListener('click', function(ev) {\n 117- ev.stopPropagation();\n 118: if (typeof openShareModal === 'function') openShareModal();\n 119- });\n 120-\n<\/workspace_result>","isError":false,"uuid":"grok-result-22"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"","toolCalls":[{"id":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_fyBcC","name":"Read","input":"/home/everlier/code/tracequest/src/share/scanner.js"},{"id":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_SBQJo","name":"Read","input":"/home/everlier/code/tracequest/src/share/redactor.js"},{"id":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_by04n","name":"Read","input":"/home/everlier/code/tracequest/src/share/auth.js"},{"id":"call-9c03b1ab-b92a-4d23-8314-0384c2941364-composer_call_SXlHb","name":"Grep","input":"Share|share-modal|hf-btn "}],"tokens":{"input":3934,"output":68,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-23"},{"type":"tool_result","timestamp":"2026-06-25T08:11:15.472Z","toolUseId":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_SBQJo","text":"# tracequest\n\n[![CI](https://github.com/av/tracequest/actions/workflows/ci.yml/badge.svg)](https://github.com/av/tracequest/actions/workflows/ci.yml)\n\nStop reading agent logs. Watch them.\n\n`tracequest` is a zero-dependency Node.js CLI that transforms agent session logs into self-contained, interactive HTML visualizations. It discovers sessions across multiple AI coding agents, renders detailed per-session views (with waveforms, chapters, tool usage, and stats), and provides a live web browser for exploring and searching your entire history.\n\n## Installation\n 10|\nInstall globally so the `tracequest` command is available everywhere:\n\n```bash\nnpm install -g tracequest\n```\n\nRun via npx without a permanent install:\n\n```bash\n 20|npx tracequest --help\n```\n\nAfter installation, run `tracequest --help` to see the built-in usage summary.\n\n## CLI Usage\n\ntracequest uses subcommands. Run `tracequest --help` for the built-in reference.\n\n```\n 30|tracequest — stop reading agent logs. watch the","isError":false,"uuid":"grok-result-24"},{"type":"tool_result","timestamp":"2026-06-25T08:11:15.490Z","toolUseId":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_Y3P6k","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nsrc/cli/cli-commands.js\n 448:export function showShareHelp() {\n 449- console.log(`${k.bold(\"tracequest share\")} — share a session to Gist or Hugging Face\n 450-\n 451-${k.bold(\"Usage:\")}\n 452- tracequest share <session.jsonl> [options]\n 453- tracequest share --latest [project] [options]\n 454- tracequest share --expr <expression> [options]\n 455-\n 456-${SHARE_OPTIONS_HELP}\n 457-\n 458-${k.bold(\"Examples:\")}\n 459- tracequest share session.jsonl --target gist --open\n 460- tracequest share session.jsonl --target hf --hf-repo user/tracequest-sessions --json\n 461- tracequest share --latest myproject --target gist\n 462- tracequest share --expr \"model:sonnet\" --force\n 463-\n 464-${k.dim(\"Secrets are scanned locally; matched values are redacted to [REDACTED] before upload.\")}`);\n 465- process.exit(0);\n 466-}\n 467-\n 468:function resolveShareSession(positionals, values) {\n 469- if (values.latest || value","isError":false,"uuid":"grok-result-25"},{"type":"tool_result","timestamp":"2026-06-25T08:11:15.505Z","toolUseId":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_ghtyx","text":"<workspace_result workspace_path=\"/home/everlier/code/tracequest\">\nREADME.md\n 96-- **Droid/Factory** — `~/.factory/sessions/`\n 97:- **OpenCode** — `~/.local/share/opencode/opencode.db`\n 98-- **Grok CLI** — `~/.grok/sessions/`\n 99-\n 100-Use `tracequest list` or `tracequest serve` (or the library `findSessions()`) to aggregate across every supported Source. Source is shown with color-coded badges in the UI and HTML (claude=purple, codex=teal, factory=yellow, opencode=blue, grok=red).\n 101-\n 102-## Next Steps\n\ntests/README.md\n 36-| Core CLI | [`integration.md`](integration.md) | 15 | help, version, render, messages, list, find/search/latest expr, serve smoke |\n 37:| Sharing | [`sharing-integration.md`](sharing-integration.md) | 19 | `share` CLI, bundle output, HF upload mocks, secret scan/redaction, full rule compilation audit |\n 38-| Serve HTTP | [`serve-integration.md`](serve-integration.md) | 42 | `/api/sessions`, `/api/search`, `serve --filter`, `?expr=`, `/view`, `/export`,","isError":false,"uuid":"grok-result-26"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"","toolCalls":[{"id":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_Y3P6k","name":"Grep","input":"resolveShareSession|showShareHelp "},{"id":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_SBQJo","name":"Read","input":"/home/everlier/code/tracequest/README.md"},{"id":"call-988fe07c-aad0-4c9a-857f-abd5336cca4a-composer_call_ghtyx","name":"Grep","input":"share "}],"tokens":{"input":3934,"output":38,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"tool_use","uuid":"grok-asst-27"},{"type":"assistant","timestamp":"2026-06-25T08:11:04.224Z","text":"The **share** feature lets you publish an agent session as a self-contained HTML page to the web — after scanning and redacting secrets locally. Raw session data never leaves your machine in plaintext.\n\n## What it does\n\nTracequest already renders sessions as interactive, standalone HTML. Share takes that one step further: it runs a safety pipeline, then uploads the cleaned HTML to either **GitHub Gist** (default) or **Hugging Face Hub**. The result is a public URL anyone can open in a browser — ","toolCalls":[],"tokens":{"input":3934,"output":125,"cacheHit":0,"cacheWrite":0,"estimated":true},"stopReason":"end_turn","uuid":"grok-asst-28"}],"stats":{"userMessages":1,"assistantTurns":7,"totalInputTokens":27538,"totalOutputTokens":473,"totalCacheHit":0,"errors":0,"toolCounts":{"Grep":7,"Bash":1,"Glob":2,"Read":11},"tokensEstimated":true}};
(function() {
var LOWER_NATIVE_THRESHOLD = 4096;
function needsLocaleFold(s) {
for (let i = 0; i < s.length; i++) {
if (s.charCodeAt(i) > 0x7f) return true;
}
return false;
}
function indexOfLower(haystack, needleLower, fromIndex = 0) {
if (!needleLower) {
const hlen = haystack?.length ?? 0;
return fromIndex >= 0 && fromIndex <= hlen ? fromIndex : -1;
}
if (!haystack) return -1;
if (
haystack.length > LOWER_NATIVE_THRESHOLD ||
needsLocaleFold(haystack) ||
needsLocaleFold(needleLower)
) {
return haystack.toLowerCase().indexOf(needleLower, fromIndex);
}
const hlen = haystack.length;
const nlen = needleLower.length;
const start = fromIndex < 0 ? 0 : fromIndex;
if (!nlen) return start < hlen ? start : hlen;
if (start + nlen > hlen) return -1;
outer: for (let i = start; i <= hlen - nlen; i++) {
for (let j = 0; j < nlen; j++) {
let hc = haystack.charCodeAt(i + j);
if (hc >= 0x41 && hc <= 0x5a) hc += 0x20;
if (hc !== needleLower.charCodeAt(j)) continue outer;
}
return i;
}
return -1;
}
function includesLower(haystack, needleLower) {
return indexOfLower(haystack, needleLower) !== -1;
}
function getModelRates(model) {
const isOpus = includesLower(model || '', 'opus');
return {
inRate: isOpus ? 15 : 3,
outRate: isOpus ? 75 : 15,
cacheReadRate: isOpus ? 1.5 : 0.3,
};
}
function estimateParsedStatsCost(model, stats) {
if (!stats) return 0;
return estimateChapterTokenCost(model, {
input: stats.totalInputTokens || 0,
cacheHit: stats.totalCacheHit || 0,
output: stats.totalOutputTokens || 0,
});
}
function estimateChapterTokenCost(model, tokens) {
if (!tokens) return 0;
const { inRate, outRate, cacheReadRate } = getModelRates(model);
const input = tokens.input || 0;
const cacheHit = tokens.cacheHit || 0;
const output = tokens.output || 0;
const uncachedInput = Math.max(0, input - cacheHit);
return (uncachedInput * inRate + cacheHit * cacheReadRate + output * outRate) / 1e6;
}
function fmtTokens(n, opts) {
const zeroLabel = opts && opts.zeroLabel !== undefined ? opts.zeroLabel : '';
if (!n) return zeroLabel;
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
return String(n);
}
function fmtCost(c, opts = {}) {
const zeroLabel = opts.zeroLabel !== undefined ? opts.zeroLabel : "";
const min = opts.min !== undefined ? opts.min : 0.01;
const prefix = opts.prefix !== undefined ? opts.prefix : "~$";
const tinyMin = opts.tinyMin !== undefined ? opts.tinyMin : 0.001;
const tinyLabel = opts.tinyLabel;
if (c == null || Number.isNaN(c)) return zeroLabel;
if (c < min) {
if (tinyLabel && c >= tinyMin) return tinyLabel;
return zeroLabel;
}
const dec =
opts.decimals !== undefined
? opts.decimals
: opts.fine && c < 0.01
? 4
: c >= 1
? 2
: 3;
return prefix + Number(c).toFixed(dec);
}
function fmtPct(n, opts = {}) {
const zeroLabel = opts.zeroLabel !== undefined ? opts.zeroLabel : "--";
const decimals = opts.decimals !== undefined ? opts.decimals : 0;
if (n == null || Number.isNaN(n)) return zeroLabel;
if (opts.hideZero && n <= 0) return zeroLabel;
return Number(n).toFixed(decimals) + "%";
}
function formatDuration(ms, opts = {}) {
const {
floorSeconds = false,
includeSeconds = false,
alwaysShowMinutes = false,
subSecondLabel = '',
zeroLabel,
} = opts;
const emptyLabel = zeroLabel !== undefined ? zeroLabel : subSecondLabel;
if (!ms || ms <= 0) return emptyLabel;
const s = floorSeconds ? Math.floor(ms / 1000) : Math.round(ms / 1000);
if (s < 60) {
if (s === 0) return subSecondLabel || '0s';
return s + 's';
}
const m = Math.floor(s / 60);
if (m < 60) {
return m + 'm' + (includeSeconds && s % 60 > 0 ? ' ' + (s % 60) + 's' : '');
}
const h = Math.floor(m / 60);
const rm = m % 60;
if (rm === 0 && !alwaysShowMinutes) return h + 'h';
return h + 'h ' + rm + 'm';
}
function fmtMcpName(name) {
if (typeof name !== "string" || !name.startsWith("mcp__")) return name;
const rest = name.slice(5);
const idx = rest.indexOf("__");
if (idx < 0) return rest;
return rest.slice(0, idx).replace(/_/g, " ") + ": " + rest.slice(idx + 2).replace(/_/g, " ");
}
const app = document.getElementById('app');
const session = SESSION;
const events = session.events;
let expandedSet = new Set();
function jumpToChapter(idx) {
var target = document.getElementById('chapter-' + idx);
if (!target) return;
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.classList.remove('highlight');
void target.offsetWidth;
target.classList.add('highlight');
}
function setupHiDpiCanvas(canvas, logicalWidth, logicalHeight) {
const dpr = window.devicePixelRatio || 1;
canvas.width = logicalWidth * dpr;
canvas.height = logicalHeight * dpr;
canvas.style.height = logicalHeight + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
return ctx;
}
function formatTime(iso) {
return new Date(iso).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function h(tag, attrs, ...children) {
const el = document.createElement(tag);
if (attrs) {
for (const k in attrs) {
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
const v = attrs[k];
if (k === 'className') el.className = v;
else if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
else el.setAttribute(k, v);
}
}
children.flat().forEach(c => {
if (c == null) return;
el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
});
return el;
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function sumToolCounts(counts = {}) {
let n = 0;
for (const k in counts) {
if (Object.prototype.hasOwnProperty.call(counts, k)) n += counts[k];
}
return n;
}
function readUrlState() {
var params = new URLSearchParams(window.location.search);
var hash = window.location.hash;
var toolParam = params.get('tool');
if (toolParam) {
toolParam.split(',').forEach(function(t) { if (t.trim()) activeToolFilters.add(t.trim()); });
}
var qParam = params.get('q');
if (qParam) searchQuery = qParam;
if (hash && hash.match(/^#chapter-(\d+)$/)) {
var chIdx = parseInt(hash.slice(9), 10);
if (!isNaN(chIdx) && chIdx >= 0) {
expandedSet.add('ch' + chIdx);
requestAnimationFrame(function() {
requestAnimationFrame(function() {
var el = document.getElementById('chapter-' + chIdx);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
}
}
}
function buildFilterSearchParams() {
var params = new URLSearchParams(window.location.search);
var base = new URLSearchParams();
if (params.get('path')) base.set('path', params.get('path'));
if (params.get('source')) base.set('source', params.get('source'));
if (activeToolFilters.size > 0) base.set('tool', Array.from(activeToolFilters).join(','));
if (searchQuery) base.set('q', searchQuery);
return base;
}
function updateUrl() {
var base = buildFilterSearchParams();
var hash = window.location.hash || '';
var url = window.location.pathname + '?' + base.toString() + hash;
history.replaceState(null, '', url);
}
function setChapterHash(idx) {
var base = buildFilterSearchParams();
var url = window.location.pathname + '?' + base.toString() + '#chapter-' + idx;
history.replaceState(null, '', url);
}
function clearChapterHash() {
var base = buildFilterSearchParams();
var url = window.location.pathname + '?' + base.toString();
history.replaceState(null, '', url);
}
function getPermalink(chapterIdx) {
var base = new URL(window.location.pathname, window.location.origin);
var params = buildFilterSearchParams();
base.search = params.toString() ? '?' + params.toString() : '';
base.hash = 'chapter-' + chapterIdx;
return base.toString();
}
function renderHeader() {
const s = session.stats;
const sourceColors = { claude: '#a78bfa', codex: '#59d4a0', factory: '#e0c45e', opencode: '#6ba4e8', grok: '#f07070' };
const srcColor = sourceColors[session.source] || '#888';
const srcBadge = session.source && session.source !== 'claude'
? h('span', { style: 'background:' + srcColor + ';color:#111;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;padding:2px 7px;border-radius:4px;margin-left:8px;' }, session.source)
: null;
const params = new URLSearchParams(window.location.search);
const exportUrl = '/export?' + params.toString();
const downloadIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
downloadIcon.setAttribute('viewBox', '0 0 24 24');
downloadIcon.innerHTML = '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>';
const exportBtn = h('a', { className: 'export-btn', href: exportUrl, title: 'Download as standalone HTML file' });
exportBtn.appendChild(downloadIcon);
exportBtn.appendChild(document.createTextNode('Export'));
const printIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
printIcon.setAttribute('viewBox', '0 0 24 24');
printIcon.innerHTML = '<polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>';
const printBtn = h('button', { className: 'print-btn', title: 'Print or save as PDF' });
printBtn.appendChild(printIcon);
printBtn.appendChild(document.createTextNode('Print'));
printBtn.addEventListener('click', function(ev) {
ev.stopPropagation();
expandAllChapterDetails();
window.print();
});
const mdUrl = '/markdown?' + params.toString();
const mdIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
mdIcon.setAttribute('viewBox', '0 0 24 24');
mdIcon.innerHTML = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>';
const mdBtn = h('a', { className: 'md-btn', href: mdUrl, title: 'Download as markdown document' });
mdBtn.appendChild(mdIcon);
mdBtn.appendChild(document.createTextNode('Markdown'));
const shareIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
shareIcon.setAttribute('viewBox', '0 0 24 24');
shareIcon.innerHTML = '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>';
const shareBtn = h('button', { className: 'hf-btn', title: 'Share session to Gist or Hugging Face' });
shareBtn.appendChild(shareIcon);
shareBtn.appendChild(document.createTextNode('Share'));
shareBtn.addEventListener('click', function(ev) {
ev.stopPropagation();
if (typeof openShareModal === 'function') openShareModal();
});
return h('div', { className: 'header' },
h('div', { className: 'header-top' },
h('div', { className: 'header-title' },
'tracequest',
h('span', null, session.sessionId?.slice(0, 8) || 'session'),
srcBadge
),
h('div', { className: 'header-actions' }, printBtn, mdBtn, exportBtn, shareBtn)
),
h('div', { className: 'meta-grid' },
metaItem('model', session.model || '—'),
metaItem('cwd', session.cwd || '—'),
metaItem('branch', session.gitBranch || '—'),
metaItem('duration', session.durationMs > 0 ? formatDuration(session.durationMs) : '—'),
metaItem('started', session.startTime && session.eventCount > 0 ? new Date(session.startTime).toLocaleString() : '—'),
)
);
}
function metaItem(label, value) {
return h('div', { className: 'meta-item' },
h('span', { className: 'meta-label' }, label),
h('span', { className: 'meta-value' }, value)
);
}
function renderStats() {
const s = session.stats;
const hasTokens = s.totalInputTokens > 0 || s.totalOutputTokens > 0;
const est = s.tokensEstimated ? '~' : '';
const bar = h('div', { className: 'stats-bar' },
stat(s.userMessages, 'prompts', ''),
stat(s.assistantTurns, 'turns', ''),
stat(sumToolCounts(s.toolCounts), 'tool calls', ''),
stat(hasTokens ? est + fmtTokens(s.totalInputTokens) : '—', 'input tok', ''),
stat(hasTokens ? est + fmtTokens(s.totalOutputTokens) : '—', 'output tok', ''),
stat(s.totalCacheHit && s.totalInputTokens ? ((s.totalCacheHit / s.totalInputTokens * 100) | 0) + '%' : '—', 'cache hit', 'green'),
stat(s.errors, 'errors', s.errors > 0 ? 'red' : ''),
);
if (s.tokensEstimated) {
bar.appendChild(h('div', { className: 'stats-note' }, '~ token counts estimated from content length'));
}
return bar;
}
function stat(value, label, cls) {
return h('div', { className: 'stat' },
h('div', { className: 'stat-value' + (cls ? ' ' + cls : '') }, String(value)),
h('div', { className: 'stat-label' }, label),
);
}
function aggregateChapterMetrics(chapters) {
var cleanCount = 0;
var correctedCount = 0;
var strugglingCount = 0;
var totalErrors = 0;
var totalRetries = 0;
var totalSelfCorrections = 0;
var totalCommits = 0;
var wastefulCount = 0;
var totalWasteTokens = 0;
var allFiles = new Set();
for (var i = 0; i < chapters.length; i++) {
var ch = chapters[i];
var outcome = ch.outcome;
if (outcome === 'clean') cleanCount++;
else if (outcome === 'corrected') correctedCount++;
else if (outcome === 'struggling') strugglingCount++;
totalErrors += ch.errors;
totalRetries += ch.retries || 0;
totalSelfCorrections += ch.selfCorrections || 0;
var gitOps = ch.gitOps;
for (var j = 0; j < gitOps.length; j++) {
if (gitOps[j].type === 'commit') totalCommits++;
}
var fileKeys = chapterFileKeys(ch);
for (var k = 0; k < fileKeys.length; k++) allFiles.add(fileKeys[k]);
var eff = ch.efficiency;
if (eff && eff.isWasteful) {
wastefulCount++;
totalWasteTokens += eff.wasteTokens || 0;
}
}
return {
cleanCount: cleanCount,
correctedCount: correctedCount,
strugglingCount: strugglingCount,
totalErrors: totalErrors,
totalRetries: totalRetries,
totalSelfCorrections: totalSelfCorrections,
totalCommits: totalCommits,
wastefulCount: wastefulCount,
totalWasteTokens: totalWasteTokens,
allFiles: allFiles,
};
}
function computeSessionGrade(chapters, metrics) {
const s = session.stats;
const totalChapters = chapters.length;
if (totalChapters === 0) return null;
const m = metrics || aggregateChapterMetrics(chapters);
const totalToolCalls = sumToolCounts(s.toolCounts);
const totalErrors = m.totalErrors;
const totalRetries = m.totalRetries;
const totalSelfCorrections = m.totalSelfCorrections;
const cleanCount = m.cleanCount;
const correctedCount = m.correctedCount;
const isSubagent = totalChapters === 1;
// Factor 1: Error rate (35%) — gentler curve, 5% errors is still good
// 0% = 100, 5% = 88, 10% = 75, 20% = 50, 40%+ = 0
const errorRatio = totalToolCalls > 0 ? totalErrors / totalToolCalls : 0;
const errorScore = Math.max(0, 100 - errorRatio * 250);
// Factor 2: Chapter quality (20%) — count corrected chapters as partial credit
// Subagent sessions (1 chapter): corrected = 80% credit to avoid 0 score
// Multi-chapter: clean = 100%, corrected = 60%, struggling = 0%
const qualityNumerator = cleanCount + correctedCount * (isSubagent ? 0.8 : 0.6);
const qualityRatio = qualityNumerator / totalChapters;
const qualityScore = Math.min(100, qualityRatio * 100);
// Factor 3: Retry ratio (10%) — only penalize excessive retries (>20% of tool calls)
// Normal iteration is expected; only flag when >20% of calls are retries
// <20% = 100, 30% = 67, 50% = 0
const retryRatio = totalToolCalls > 0 ? totalRetries / totalToolCalls : 0;
const retryScore = retryRatio <= 0.20 ? 100
: Math.max(0, 100 - (retryRatio - 0.20) * 333);
// Factor 4: Self-correction rate (15%) — higher is better when errors exist
const correctionScore = totalErrors === 0 ? 100
: Math.min(100, (totalSelfCorrections / totalErrors) * 100);
// Factor 5: Cache efficiency (20%)
// Neutral (75) when insufficient input data (<10K tokens) to judge cache usage
const cacheHit = s.totalCacheHit || 0;
const totalInput = s.totalInputTokens || 0;
const cacheRatio = totalInput > 0 ? cacheHit / totalInput : 0;
const cacheScore = totalInput < 10000 ? 75 : Math.min(100, cacheRatio * 125);
// 80%+ cache hit = 100, 0% = 0
const score = Math.round(
errorScore * 0.35 +
qualityScore * 0.20 +
retryScore * 0.10 +
correctionScore * 0.15 +
cacheScore * 0.20
);
const clampedScore = Math.max(0, Math.min(100, score));
let letter, letterCls;
if (clampedScore >= 90) { letter = 'A'; letterCls = 'grade-a'; }
else if (clampedScore >= 80) { letter = 'B'; letterCls = 'grade-b'; }
else if (clampedScore >= 70) { letter = 'C'; letterCls = 'grade-c'; }
else if (clampedScore >= 60) { letter = 'D'; letterCls = 'grade-d'; }
else { letter = 'F'; letterCls = 'grade-f'; }
const notes = [];
if (errorScore >= 90) notes.push('low error rate');
else if (errorScore < 50) notes.push('high error rate');
if (qualityRatio >= 0.8) notes.push('mostly clean');
else if (qualityRatio < 0.4 && totalChapters > 2) notes.push('many troubled chapters');
if (cacheScore >= 80 && totalInput > 0) notes.push('good cache usage');
else if (cacheScore < 40 && totalInput > 0) notes.push('low cache efficiency');
if (correctionScore >= 80 && totalErrors > 0) notes.push('good recovery');
else if (correctionScore < 30 && totalErrors > 2) notes.push('poor error recovery');
if (retryScore >= 90) notes.push('few retries');
else if (retryScore < 50) notes.push('many retries');
return {
score: clampedScore,
letter,
letterCls,
note: notes.slice(0, 3).join(', '),
factors: [
{ name: 'errors', score: Math.round(errorScore), weight: 35 },
{ name: 'quality', score: Math.round(qualityScore), weight: 20 },
{ name: 'retries', score: Math.round(retryScore), weight: 10 },
{ name: 'recovery', score: Math.round(correctionScore), weight: 15 },
{ name: 'cache', score: Math.round(cacheScore), weight: 20 },
],
};
}
function renderSessionSummary() {
const s = session.stats;
const chapters = getChapters();
const m = aggregateChapterMetrics(chapters);
const wrap = h('div', { className: 'session-summary' });
const items = [];
if (session.durationMs > 0) {
items.push(summaryItem('duration', formatDuration(session.durationMs), 'ss-duration'));
}
const totalCost = estimateParsedStatsCost(session.model, s);
const costLabel = fmtCost(totalCost);
if (costLabel) {
items.push(summaryItem('cost', costLabel, 'ss-cost'));
}
const totalTok = s.totalInputTokens + s.totalOutputTokens;
if (totalTok > 0) {
items.push(summaryItem('tokens', fmtTokens(totalTok), ''));
}
if (s.errors > 0) {
items.push(summaryItem('errors', String(s.errors), 'ss-errors'));
}
if (m.totalCommits > 0) {
items.push(summaryItem('commits', String(m.totalCommits), 'ss-commits'));
}
if (m.allFiles.size > 0) {
items.push(summaryItem('files', String(m.allFiles.size), ''));
}
const cleanCount = m.cleanCount;
const correctedCount = m.correctedCount;
const strugglingCount = m.strugglingCount;
if (chapters.length > 1) {
const qualityParts = [];
if (cleanCount > 0) qualityParts.push(cleanCount + ' clean');
if (correctedCount > 0) qualityParts.push(correctedCount + ' corrected');
if (strugglingCount > 0) qualityParts.push(strugglingCount + ' struggling');
if (qualityParts.length > 0 && (correctedCount > 0 || strugglingCount > 0)) {
const qualityCls = strugglingCount > correctedCount ? 'ss-errors'
: correctedCount > 0 ? 'ss-quality-mixed' : '';
items.push(summaryItem('quality', qualityParts.join(', '), qualityCls));
}
}
if (chapters._depSummary && chapters._depSummary.chains > 0) {
const ds = chapters._depSummary;
const chainLabel = ds.chains + ' workflow chain' + (ds.chains > 1 ? 's' : '') +
' (' + ds.chaptersInChains + ' of ' + chapters.length + ' ch)';
items.push(summaryItem('workflows', chainLabel, ''));
}
if (m.wastefulCount > 0) {
// Use session's actual cost-per-token ratio instead of raw rate average
// (averaging in/out rates wildly overestimates because most tokens are cheap cached input)
const costPerToken = totalTok > 0 && totalCost > 0 ? totalCost / totalTok : 0;
const wasteCost = m.totalWasteTokens * costPerToken;
let wasteLabel = m.wastefulCount + ' wasteful ch';
if (m.totalWasteTokens > 0) wasteLabel += ' · ~' + fmtTokens(m.totalWasteTokens) + ' wasted';
const wasteCostStr = fmtCost(wasteCost);
if (wasteCostStr) wasteLabel += ' (' + wasteCostStr + ')';
items.push(summaryItem('waste', wasteLabel, 'ss-waste'));
}
const grade = computeSessionGrade(chapters, m);
if (grade) {
const gradeEl = h('div', { className: 'session-grade' });
gradeEl.appendChild(h('span', { className: 'session-grade-letter ' + grade.letterCls }, grade.letter));
const detailEl = h('div', { className: 'session-grade-detail' });
detailEl.appendChild(h('span', { className: 'session-grade-score' }, grade.score + '/100'));
const breakdownEl = h('div', { className: 'session-grade-breakdown' });
for (const f of grade.factors) {
const factorEl = h('span', { className: 'session-grade-factor' });
factorEl.textContent = f.name + ' ';
const barWrap = h('span', { className: 'session-grade-factor-bar' });
const fillCls = f.score >= 70 ? 'fill-good' : f.score >= 40 ? 'fill-ok' : 'fill-bad';
const fill = h('span', { className: 'session-grade-factor-fill ' + fillCls });
fill.style.width = Math.max(2, f.score) + '%';
barWrap.appendChild(fill);
factorEl.appendChild(barWrap);
breakdownEl.appendChild(factorEl);
}
detailEl.appendChild(breakdownEl);
if (grade.note) {
detailEl.appendChild(h('span', { className: 'session-grade-note' }, grade.note));
}
gradeEl.appendChild(detailEl);
wrap.appendChild(gradeEl);
if (items.length > 0) {
wrap.appendChild(h('div', { className: 'session-summary-sep' }));
}
}
for (let i = 0; i < items.length; i++) {
wrap.appendChild(items[i]);
if (i < items.length - 1) {
wrap.appendChild(h('div', { className: 'session-summary-sep' }));
}
}
return items.length > 0 || grade ? wrap : h('div');
}
function summaryItem(label, value, cls) {
return h('div', { className: 'session-summary-item' + (cls ? ' ' + cls : '') },
h('span', { className: 'ss-label' }, label),
h('span', { className: 'ss-value' }, value)
);
}
function renderActivityTimeline() {
const timed = [];
for (const e of events) {
if (!e.timestamp) continue;
const ms = new Date(e.timestamp).getTime();
if (isNaN(ms)) continue;
timed.push({ ms, type: e.type });
}
if (timed.length < 3) return h('div');
timed.sort(function(a, b) { return a.ms - b.ms; });
const sessionStart = timed[0].ms;
const sessionEnd = timed[timed.length - 1].ms;
const totalSpan = sessionEnd - sessionStart;
if (totalSpan < 10000) return h('div'); // less than 10s — not useful
const chapters = getChapters();
const chapterRanges = [];
for (const ch of chapters) {
const s = ch.timestamp ? new Date(ch.timestamp).getTime() : 0;
const e = ch.endTimestamp ? new Date(ch.endTimestamp).getTime() : s;
if (s > 0) chapterRanges.push({ start: s, end: e || s });
}
const GAP_THRESHOLD = 120000; // 2 minutes
const IDLE_LABEL_THRESHOLD = 300000; // 5 minutes — label these
const gaps = [];
for (let i = 1; i < timed.length; i++) {
const delta = timed[i].ms - timed[i - 1].ms;
if (delta >= GAP_THRESHOLD) {
gaps.push({ start: timed[i - 1].ms, end: timed[i].ms, duration: delta });
}
}
const segments = [];
let segStart = sessionStart;
for (const gap of gaps) {
if (gap.start > segStart) {
segments.push({ start: segStart, end: gap.start, type: 'active' });
}
segments.push({ start: gap.start, end: gap.end, type: 'idle', duration: gap.duration });
segStart = gap.end;
}
if (segStart < sessionEnd) {
segments.push({ start: segStart, end: sessionEnd, type: 'active' });
}
function formatClockTime(ms) {
const d = new Date(ms);
let h = d.getHours();
const m = d.getMinutes();
const ampm = h >= 12 ? 'PM' : 'AM';
h = h % 12 || 12;
return h + ':' + (m < 10 ? '0' : '') + m + ' ' + ampm;
}
const wrap = h('div', { className: 'activity-timeline' });
wrap.appendChild(h('div', { className: 'activity-timeline-label' },
'activity timeline — colored segments show active work, dim gaps show idle periods'));
const canvas = document.createElement('canvas');
canvas.className = 'activity-timeline-canvas';
const HEIGHT = 48;
wrap.appendChild(canvas);
const tooltip = h('div', { className: 'activity-timeline-tooltip' });
wrap.appendChild(tooltip);
function drawTimeline() {
const W = canvas.parentElement?.clientWidth || 800;
const ctx = setupHiDpiCanvas(canvas, W, HEIGHT);
ctx.fillStyle = '#111113';
ctx.fillRect(0, 0, W, HEIGHT);
function timeToX(ms) {
return ((ms - sessionStart) / totalSpan) * W;
}
const barY = 12;
const barH = 20;
for (const seg of segments) {
const x1 = timeToX(seg.start);
const x2 = timeToX(seg.end);
const w = Math.max(1, x2 - x1);
if (seg.type === 'active') {
const grad = ctx.createLinearGradient(x1, barY, x1, barY + barH);
grad.addColorStop(0, 'rgba(139, 124, 246, 0.7)');
grad.addColorStop(1, 'rgba(139, 124, 246, 0.35)');
ctx.fillStyle = grad;
ctx.beginPath();
if (ctx.roundRect) { ctx.roundRect(x1, barY, w, barH, 3); }
else { ctx.rect(x1, barY, w, barH); }
ctx.fill();
} else {
ctx.fillStyle = 'rgba(255, 255, 255, 0.02)';
ctx.fillRect(x1, barY, w, barH);
ctx.save();
ctx.setLineDash([3, 4]);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x1, barY + barH / 2);
ctx.lineTo(x1 + w, barY + barH / 2);
ctx.stroke();
ctx.restore();
if (seg.duration >= IDLE_LABEL_THRESHOLD && w > 30) {
var gapMinutes = Math.round(seg.duration / 60000);
var gapLabel = gapMinutes >= 60 ? (gapMinutes / 60).toFixed(1) + 'h' : gapMinutes + 'm';
ctx.font = '9px ' + getComputedStyle(document.body).getPropertyValue('--mono').trim().split(',')[0].replace(/'/g, '');
ctx.fillStyle = 'rgba(232, 164, 76, 0.8)';
ctx.textAlign = 'center';
ctx.fillText(gapLabel + ' idle', x1 + w / 2, barY + barH / 2 + 3);
}
}
}
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
for (let i = 1; i < chapterRanges.length; i++) {
const x = timeToX(chapterRanges[i].start);
ctx.beginPath();
ctx.moveTo(x, barY - 2);
ctx.lineTo(x, barY + barH + 2);
ctx.stroke();
}
ctx.font = '8px ' + getComputedStyle(document.body).getPropertyValue('--mono').trim().split(',')[0].replace(/'/g, '');
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
ctx.textAlign = 'center';
// Only label if chapters won't overlap (min 20px apart)
var prevLabelX = -30;
for (let i = 0; i < chapterRanges.length; i++) {
const x = timeToX(chapterRanges[i].start);
if (x - prevLabelX >= 20) {
ctx.fillText(String(i + 1), x, barY - 4);
prevLabelX = x;
}
}
const dotY = barY + barH + 6;
const bucketCount = Math.min(W, 200);
const bucketW = totalSpan / bucketCount;
const buckets = new Array(bucketCount).fill(0);
for (const t of timed) {
const idx = Math.min(bucketCount - 1, Math.floor((t.ms - sessionStart) / bucketW));
buckets[idx]++;
}
const maxBucket = Math.max(...buckets, 1);
for (let i = 0; i < bucketCount; i++) {
if (buckets[i] === 0) continue;
const alpha = 0.2 + 0.8 * (buckets[i] / maxBucket);
const x = (i / bucketCount) * W;
const w = Math.max(1, W / bucketCount - 0.5);
ctx.fillStyle = 'rgba(139, 124, 246, ' + alpha.toFixed(2) + ')';
ctx.fillRect(x, dotY, w, 3);
}
}
drawTimeline();
window.addEventListener('resize', drawTimeline);
canvas.addEventListener('mousemove', function(ev) {
const rect = canvas.getBoundingClientRect();
const x = ev.clientX - rect.left;
const W = rect.width;
const ratio = x / W;
const hoverMs = sessionStart + ratio * totalSpan;
const hoverTime = formatClockTime(hoverMs);
var segInfo = '';
for (const seg of segments) {
if (hoverMs >= seg.start && hoverMs <= seg.end) {
if (seg.type === 'idle') {
var gapMin = Math.round(seg.duration / 60000);
segInfo = ' \u2014 idle gap (' + (gapMin >= 60 ? (gapMin / 60).toFixed(1) + 'h' : gapMin + 'm') + ')';
} else {
segInfo = ' \u2014 active';
}
break;
}
}
var chLabel = '';
for (let i = 0; i < chapterRanges.length; i++) {
var cr = chapterRanges[i];
var nextStart = i + 1 < chapterRanges.length ? chapterRanges[i + 1].start : sessionEnd;
if (hoverMs >= cr.start && hoverMs < nextStart) {
chLabel = ' \u2014 ch ' + (i + 1);
break;
}
}
tooltip.style.display = 'block';
tooltip.textContent = hoverTime + chLabel + segInfo;
var left = ev.clientX - rect.left + 12;
if (left + 160 > W) left = ev.clientX - rect.left - 160;
tooltip.style.left = left + 'px';
tooltip.style.top = '-28px';
});
canvas.addEventListener('mouseleave', function() {
tooltip.style.display = 'none';
});
const timesRow = h('div', { className: 'activity-timeline-times' });
const labelCount = Math.min(8, Math.max(2, Math.floor(totalSpan / 300000))); // ~5min minimum spacing
for (let i = 0; i <= labelCount; i++) {
const t = sessionStart + (i / labelCount) * totalSpan;
timesRow.appendChild(h('span', null, formatClockTime(t)));
}
wrap.appendChild(timesRow);
const legend = h('div', { className: 'activity-timeline-legend' });
function legendItem(color, text) {
const dot = h('span', { className: 'activity-timeline-legend-dot' });
dot.style.background = color;
return h('span', { className: 'activity-timeline-legend-item' }, dot, text);
}
legend.appendChild(legendItem('rgba(139, 124, 246, 0.7)', 'active'));
legend.appendChild(legendItem('rgba(232, 164, 76, 0.6)', 'idle >5m'));
legend.appendChild(legendItem('rgba(255, 255, 255, 0.15)', 'chapter boundary'));
var totalIdleMs = gaps.reduce(function(s, g) { return s + g.duration; }, 0);
var activeMs = totalSpan - totalIdleMs;
if (totalIdleMs > 0) {
var activeStr = formatDuration(activeMs);
var idleStr = formatDuration(totalIdleMs);
legend.appendChild(h('span', { className: 'activity-timeline-legend-item', style: 'margin-left: auto; color: var(--fg2);' },
'active: ' + activeStr + ' · idle: ' + idleStr + ' (' + Math.round(totalIdleMs / totalSpan * 100) + '%)'));
}
wrap.appendChild(legend);
return wrap;
}
const TOOL_COLORS = {
Bash: '#59d4a0',
Edit: '#e0c45e',
Write: '#d89660',
Read: '#6ba4e8',
Agent: '#a78bfa',
Grep: '#7a7a85',
Glob: '#7a7a85',
Skill: '#c88abd',
WebFetch: '#6ba4e8',
WebSearch: '#6ba4e8',
ToolSearch: '#7a7a85',
_mcp: '#5dadec',
_text: '#a78bfa',
_user: '#6ba4e8',
_error: '#f07070',
};
function getToolColor(name) {
if (TOOL_COLORS[name]) return TOOL_COLORS[name];
if (typeof name === 'string' && name.startsWith('mcp__')) return TOOL_COLORS._mcp;
return TOOL_COLORS._text;
}
function renderErrorSummary() {
const chapters = getChapters();
const totalErrors = chapters.reduce((sum, ch) => sum + ch.errors, 0);
if (totalErrors === 0) return h('div');
const toolErrors = {};
for (const ch of chapters) {
for (const tool in ch.errorTools) {
if (!Object.prototype.hasOwnProperty.call(ch.errorTools, tool)) continue;
const count = ch.errorTools[tool];
toolErrors[tool] = (toolErrors[tool] || 0) + count;
}
}
const streaks = [];
let streakStart = -1;
let errorChapterCount = 0;
for (let i = 0; i < chapters.length; i++) {
if (chapters[i].errors > 0) {
errorChapterCount++;
if (streakStart < 0) streakStart = i;
} else {
if (streakStart >= 0 && i - streakStart >= 2) {
streaks.push({ start: streakStart, end: i - 1, length: i - streakStart });
}
streakStart = -1;
}
}
if (streakStart >= 0 && chapters.length - streakStart >= 2) {
streaks.push({ start: streakStart, end: chapters.length - 1, length: chapters.length - streakStart });
}
const inStreak = new Set();
for (const s of streaks) {
for (let i = s.start; i <= s.end; i++) inStreak.add(i);
}
const wrap = h('div', { className: 'error-summary' });
const header = h('div', { className: 'error-summary-header' });
header.appendChild(h('span', { className: 'error-summary-title' }, 'errors'));
header.appendChild(h('span', { className: 'error-summary-count' },
totalErrors + ' error' + (totalErrors !== 1 ? 's' : '') +
' across ' + errorChapterCount + ' chapter' + (errorChapterCount !== 1 ? 's' : '')
));
wrap.appendChild(header);
const body = h('div', { className: 'error-summary-body' });
const toolNames = [];
for (const tool in toolErrors) {
if (Object.prototype.hasOwnProperty.call(toolErrors, tool)) toolNames.push(tool);
}
toolNames.sort((a, b) => toolErrors[b] - toolErrors[a]);
if (toolNames.length > 0) {
const toolList = h('div', { className: 'error-tools-list' });
for (let ti = 0; ti < Math.min(toolNames.length, 6); ti++) {
const tool = toolNames[ti];
const count = toolErrors[tool];
var errToolLabel = fmtMcpName(tool);
toolList.appendChild(h('span', { className: 'error-tool-chip' }, errToolLabel + ' ' + count));
}
body.appendChild(toolList);
}
if (streaks.length > 0) {
const streakWrap = h('div', { className: 'error-streaks' });
for (const s of streaks) {
const badge = h('span', {
className: 'error-streak-badge',
onClick: (ev) => {
ev.stopPropagation();
jumpToChapter(s.start);
}
}, 'streak: ch ' + (s.start + 1) + '-' + (s.end + 1) + ' (' + s.length + ' chapters)');
streakWrap.appendChild(badge);
}
body.appendChild(streakWrap);
}
wrap.appendChild(body);
const timeline = h('div', { className: 'error-timeline' });
for (let i = 0; i < chapters.length; i++) {
const ch = chapters[i];
const hasErr = ch.errors > 0;
const isInStreak = inStreak.has(i);
const dotCls = hasErr ? (isInStreak ? 'error-dot streak' : 'error-dot has-error') : 'error-dot no-error';
const dot = h('div', {
className: dotCls,
title: 'Ch ' + (i + 1) + (hasErr ? ': ' + ch.errors + ' error' + (ch.errors > 1 ? 's' : '') : ''),
onClick: (ev) => {
ev.stopPropagation();
jumpToChapter(i);
}
});
if (hasErr && ch.errors > 1) {
const size = Math.min(14, 8 + ch.errors);
dot.style.width = size + 'px';
dot.style.height = size + 'px';
}
timeline.appendChild(dot);
}
wrap.appendChild(timeline);
return wrap;
}
function chapterFileKeys(ch) {
return ch._fileKeys || Object.keys(ch.files);
}
function chapterToolKeys(ch) {
return ch._toolKeys || Object.keys(ch.toolCounts);
}
const FIRST_PROMPT_MAX_LEN = 200;
function safeSlice(str, maxLen) {
if (typeof str !== "string") str = String(str ?? "");
if (str.length <= maxLen) return str;
const sliced = str.slice(0, maxLen);
// If we cut in the middle of a surrogate pair, drop the lone high surrogate
const last = sliced.charCodeAt(maxLen - 1);
if (last >= 0xD800 && last <= 0xDBFF) return sliced.slice(0, -1);
return sliced;
}
function shortToolPath(input) {
if (!input || typeof input !== "string") return "";
const last = input.lastIndexOf("/");
if (last < 0) return input;
const prev = last > 0 ? input.lastIndexOf("/", last - 1) : -1;
return prev < 0 ? input : input.slice(prev + 1);
}
function parseWebSearchLinks(output) {
if (!output) return [];
const linkRe = /\{"title":"([^"]*?)","url":"([^"]*?)"\}/g;
const parsed = [];
let m;
while ((m = linkRe.exec(output)) !== null) {
parsed.push({ title: m[1], url: m[2] });
}
return parsed;
}
function countNonEmptyLines(text) {
if (!text) return 0;
let n = 0;
let start = 0;
const len = text.length;
for (let i = 0; i <= len; i++) {
if (i === len || text[i] === "\n") {
if (text.slice(start, i).trim()) n++;
start = i + 1;
}
}
return n;
}
function joinFirstLines(text, maxLines) {
if (!text || maxLines <= 0) return "";
let lines = 0;
const len = text.length;
for (let i = 0; i < len; i++) {
if (text[i] === "\n") {
lines++;
if (lines >= maxLines) return text.slice(0, i);
}
}
return text;
}
function joinFirstNonEmptyLines(text, maxLines) {
if (!text || maxLines <= 0) return "";
const parts = [];
let start = 0;
const len = text.length;
for (let i = 0; i <= len && parts.length < maxLines; i++) {
if (i === len || text[i] === "\n") {
const line = text.slice(start, i);
if (line.trim()) parts.push(line);
start = i + 1;
}
}
return parts.join("\n");
}
function countWords(text) {
if (!text) return 0;
let n = 0;
let i = 0;
const len = text.length;
while (i < len) {
while (i < len && /\s/.test(text[i])) i++;
if (i >= len) break;
n++;
while (i < len && !/\s/.test(text[i])) i++;
}
return n;
}
function countWordsInStrings(strings) {
let total = 0;
for (let i = 0; i < strings.length; i++) total += countWords(strings[i]);
return total;
}
function extractCommitMessageFromMFlag(gitPart) {
var mMatch = gitPart.match(/-m\s+["']([^"']+)["']/);
if (!mMatch) mMatch = gitPart.match(/-m\s+"([^"]+)"/);
if (!mMatch) mMatch = gitPart.match(/-m\s+'([^']+)'/);
if (!mMatch) mMatch = gitPart.match(/-m\s+(\S+)/);
return mMatch ? mMatch[1] : null;
}
function detectGitOp(cmd, output) {
if (!cmd) return null;
var c = cmd.trim();
// Fast reject: virtually all Bash tool calls are non-git (no split/regex work).
if (c.indexOf("git ") < 0) return null;
// Single command — skip chained split when there is no && or ;
var gitPart;
var chainAt = c.indexOf("&&");
if (chainAt < 0) chainAt = c.indexOf(";");
if (chainAt < 0) {
if (!c.startsWith("git ")) return null;
gitPart = c;
} else {
var parts = c.split(/&&|;/);
gitPart = null;
for (var pi = 0; pi < parts.length; pi++) {
var p = parts[pi].trim();
if (p.startsWith("git ")) {
gitPart = p;
break;
}
}
if (!gitPart) return null;
}
// Strip git -C <path> prefix so the rest of the regex patterns work
gitPart = gitPart.replace(/^git\s+(-C\s+\S+\s+)+/, 'git ');
// git commit
if (/\bgit\s+commit\b/.test(gitPart)) {
var op = { type: 'commit', cmd: gitPart };
// Try to extract commit hash and message from output
// Typical output: "[branch abc1234] commit message here"
if (output) {
var commitMatch = output.match(/\[([^\s\]]+)\s+([a-f0-9]{7,})\]\s*(.*)/) ||
output.match(/([a-f0-9]{7,40})\]\s*(.*)/) ||
output.match(/\[\S+\s+([a-f0-9]{7,})\]\s*(.*)/);
if (commitMatch) {
// Format: [branch hash] message
op.hash = commitMatch[2] || commitMatch[1];
op.message = safeSlice((commitMatch[3] || commitMatch[2] || '').trim(), 120);
}
// Also try: "create mode" pattern or "-m" flag extraction
if (!op.hash) {
var hashLine = output.match(/([a-f0-9]{7,40})/);
if (hashLine) op.hash = hashLine[1].slice(0, 7);
}
}
if (!op.message) {
var heredocMatch = cmd.match(/<<['"]?EOF['"]?\n([\s\S]*?)\nEOF/);
if (heredocMatch) {
op.message = safeSlice(heredocMatch[1].split('\n')[0].trim(), 120);
}
}
if (!op.message) {
var msgFromFlag = extractCommitMessageFromMFlag(gitPart);
if (msgFromFlag) op.message = safeSlice(msgFromFlag, 120);
}
return op;
}
// git push
if (/\bgit\s+push\b/.test(gitPart)) {
var op = { type: 'push', cmd: gitPart };
op.tags = /--tags/.test(gitPart);
// Extract remote and branch (skip flags like --tags, -u, --force, etc.)
var pushMatch = gitPart.match(/git\s+push\s+(?:(?:--?[\w-]+)\s+)*([^\s-][\w./-]*)(?:\s+([\w./-]+))?/);
if (pushMatch) {
op.remote = pushMatch[1];
if (pushMatch[2]) op.branch = pushMatch[2];
} else {
// Try simpler: git push <remote> — scan tokens without split+filter alloc
var pi = 0;
var plen = gitPart.length;
var pn = 0;
while (pi < plen && pn < 2) {
while (pi < plen && gitPart.charAt(pi) <= " ") pi++;
if (pi >= plen) break;
var pstart = pi;
while (pi < plen && gitPart.charAt(pi) > " ") pi++;
var pt = gitPart.slice(pstart, pi);
if (pt.charAt(0) !== "-" && pt !== "git" && pt !== "push") {
if (pn === 0) op.remote = pt;
else op.branch = pt;
pn++;
}
}
}
return op;
}
// git checkout -b (create branch)
if (/\bgit\s+checkout\s+-b\b/.test(gitPart)) {
var branchMatch = gitPart.match(/git\s+checkout\s+-b\s+([\w./-]+)/);
return { type: 'branch-create', cmd: gitPart, branch: branchMatch ? branchMatch[1] : '' };
}
// git switch -c (create branch)
if (/\bgit\s+switch\s+-c\b/.test(gitPart) || /\bgit\s+switch\s+--create\b/.test(gitPart)) {
var branchMatch = gitPart.match(/git\s+switch\s+(?:-c|--create)\s+([\w./-]+)/);
return { type: 'branch-create', cmd: gitPart, branch: branchMatch ? branchMatch[1] : '' };
}
// git branch (create)
if (/\bgit\s+branch\s+[^-]/.test(gitPart) && !/\bgit\s+branch\s+-[dD]/.test(gitPart)) {
var branchMatch = gitPart.match(/git\s+branch\s+([\w./-]+)/);
if (branchMatch) return { type: 'branch-create', cmd: gitPart, branch: branchMatch[1] };
}
// git checkout (switch branch)
if (/\bgit\s+checkout\s+(?!--)\S/.test(gitPart) && !/\bgit\s+checkout\s+-b\b/.test(gitPart)) {
var branchMatch = gitPart.match(/git\s+checkout\s+([\w./-]+)/);
if (branchMatch && !branchMatch[1].includes('.')) {
return { type: 'branch-switch', cmd: gitPart, branch: branchMatch[1] };
}
}
// git switch (switch branch)
if (/\bgit\s+switch\s+(?!-c|--create)\S/.test(gitPart)) {
var branchMatch = gitPart.match(/git\s+switch\s+([\w./-]+)/);
if (branchMatch) return { type: 'branch-switch', cmd: gitPart, branch: branchMatch[1] };
}
// git merge
if (/\bgit\s+merge\b/.test(gitPart)) {
var mergeMatch = gitPart.match(/git\s+merge\s+(?:--[^\s]+\s+)*([\w./-]+)/);
return { type: 'merge', cmd: gitPart, branch: mergeMatch ? mergeMatch[1] : '' };
}
// git rebase
if (/\bgit\s+rebase\b/.test(gitPart)) {
var rebaseMatch = gitPart.match(/git\s+rebase\s+(?:--[^\s]+\s+)*([\w./-]+)/);
return { type: 'rebase', cmd: gitPart, branch: rebaseMatch ? rebaseMatch[1] : '' };
}
// git stash
if (/\bgit\s+stash\b/.test(gitPart)) {
return { type: 'stash', cmd: gitPart };
}
// git tag
if (/\bgit\s+tag\b/.test(gitPart) && !/\bgit\s+tag\s+-d/.test(gitPart)) {
var tagMatch = gitPart.match(/git\s+tag\s+(?:-[^\s]+\s+)*([\w./-]+)/);
return { type: 'tag', cmd: gitPart, tag: tagMatch ? tagMatch[1] : '' };
}
return null;
}
function buildChapterEventMaps(events) {
const resultMap = {};
const toolNameMap = {};
for (const e of events) {
if (e.type === "tool_result") {
resultMap[e.toolUseId] = e;
} else if (e.type === "assistant" && e.toolCalls) {
for (const tc of e.toolCalls) {
toolNameMap[tc.id] = tc.name;
}
}
}
return { resultMap, toolNameMap };
}
function accumulateAssistantToolCall(chapter, tc, resultMap, opts = {}) {
const trackCallSeq = !!opts.trackCallSeq;
const searchOutput = !!opts.searchOutput;
const agentResult = !!opts.agentResult;
const webFetchPreview = !!opts.webFetchPreview;
chapter.toolCounts[tc.name] = (chapter.toolCounts[tc.name] || 0) + 1;
const result = resultMap[tc.id];
const output = result ? result.text || "" : "";
const isError = result ? result.isError : false;
let filePath;
if ((tc.name === "Read" || tc.name === "Edit" || tc.name === "Write") && tc.input) {
filePath = shortToolPath(tc.input);
if (!chapter.files[filePath]) chapter.files[filePath] = { ops: [], output: "" };
chapter.files[filePath].ops.push(tc.name);
if (tc.name === "Read" && output && !chapter.files[filePath].output) {
chapter.files[filePath].output = safeSlice(joinFirstLines(output, 3), FIRST_PROMPT_MAX_LEN);
}
if (tc.diffInfo && chapter.diffs.length < 12) {
chapter.diffs.push({ path: filePath, name: tc.name, diffInfo: tc.diffInfo });
}
}
if (trackCallSeq) {
const entry = { name: tc.name, input: tc.input || "", isError, id: tc.id };
if (filePath) entry._path = filePath;
chapter._callSeq.push(entry);
}
if (tc.name === "Bash" && tc.input) {
chapter.commands.push({ cmd: tc.input, ok: !isError, output: safeSlice(output, 300) });
const gitOp = detectGitOp(tc.input, output);
if (gitOp && chapter.gitOps.length < 20) chapter.gitOps.push(gitOp);
}
if (tc.name === "Grep" && tc.input) {
const matchCount = countNonEmptyLines(output);
const entry = { query: tc.input, matches: matchCount, ok: !isError };
if (searchOutput) entry.output = safeSlice(output, FIRST_PROMPT_MAX_LEN);
chapter.searches.push(entry);
}
if (tc.webInfo && chapter.webOps.length < 10) {
const webOp = {
type: tc.webInfo.type,
url: tc.webInfo.url || "",
query: tc.webInfo.query || "",
prompt: tc.webInfo.prompt || "",
ok: !isError,
output: safeSlice(output, 500),
};
if (tc.webInfo.type === "search" && output) {
const parsedLinks = parseWebSearchLinks(output);
if (parsedLinks.length) {
webOp.results = parsedLinks.slice(0, 5);
webOp.resultCount = parsedLinks.length;
}
}
if (tc.webInfo.type === "fetch" && output) {
if (webFetchPreview) {
webOp.preview = safeSlice(joinFirstNonEmptyLines(output, 4), 300);
}
const titleMatch = output.match(/^#\s+(.+)/m);
if (titleMatch) webOp.pageTitle = safeSlice(titleMatch[1], 100);
}
chapter.webOps.push(webOp);
}
if (tc.agentInfo && chapter.agents.length < 10) {
const agent = {
description: tc.agentInfo.description || "",
prompt: tc.agentInfo.prompt || "",
subagentType: tc.agentInfo.subagentType || "",
toolName: tc.agentInfo.toolName || tc.name,
completed: !!result,
isError,
};
if (agentResult) agent.result = safeSlice(output, 400);
chapter.agents.push(agent);
}
if (tc.mcpInfo && chapter.mcpOps.length < 15) {
chapter.mcpOps.push({
server: tc.mcpInfo.server || "",
tool: tc.mcpInfo.tool || "",
rawName: tc.mcpInfo.rawName || tc.name,
params: tc.mcpInfo.params || {},
ok: !isError,
output: safeSlice(output, 400),
});
}
}
function newChapter(prompt, timestamp, variant = "html") {
const ch = {
prompt,
timestamp,
endTimestamp: timestamp,
turns: 0,
toolCounts: {},
files: {},
commands: [],
searches: [],
webOps: [],
mcpOps: [],
diffs: [],
agents: [],
errors: 0,
corrected: false,
lastAssistantText: "",
gitOps: [],
thinking: [],
};
if (variant === "markdown") {
ch.outcome = "clean";
} else {
ch.errorTools = {};
ch.tokens = { input: 0, output: 0, cacheHit: 0, cacheWrite: 0 };
ch._callSeq = [];
}
return ch;
}
function bashSplitFirstToken(s) {
if (!s) return "";
let i = 0;
const len = s.length;
while (i < len) {
const c = s.charCodeAt(i);
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break;
i++;
}
if (i > 0) return "";
const start = i;
while (i < len) {
const c = s.charCodeAt(i);
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
i++;
}
return s.slice(start, i);
}
function bashFirstTwoTokens(s) {
if (!s) return "";
let i = 0;
const len = s.length;
while (i < len) {
const c = s.charCodeAt(i);
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break;
i++;
}
if (i >= len) return "";
const start0 = i;
while (i < len) {
const c = s.charCodeAt(i);
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
i++;
}
const t0 = s.slice(start0, i);
while (i < len) {
const c = s.charCodeAt(i);
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break;
i++;
}
if (i >= len) return t0;
const start1 = i;
while (i < len) {
const c = s.charCodeAt(i);
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
i++;
}
return t0 + " " + s.slice(start1, i);
}
function compareToolTargets(input1, input2, toolName, mode, keys) {
const minLen = mode === "same" ? 0 : 1;
if (toolName === "Edit" || toolName === "Write" || toolName === "Read") {
const p1 = keys?.path1 ?? shortToolPath(input1);
const p2 = keys?.path2 ?? shortToolPath(input2);
return p1 === p2 && p1.length > minLen;
}
if (toolName === "Bash") {
const cmd1 = keys?.bash1 ?? (mode === "same" ? bashSplitFirstToken(input1) : bashFirstTwoTokens(input1));
const cmd2 = keys?.bash2 ?? (mode === "same" ? bashSplitFirstToken(input2) : bashFirstTwoTokens(input2));
return cmd1 === cmd2 && cmd1.length > minLen;
}
const limit = mode === "same" ? 30 : 40;
const s1 = input1.slice(0, limit);
const s2 = input2.slice(0, limit);
if (mode === "same") return s1 === s2;
let matches = 0;
for (let i = 0; i < Math.min(s1.length, s2.length); i++) {
if (s1[i] === s2[i]) matches++;
}
return matches / Math.max(s1.length, s2.length) > 0.5;
}
function isSameTarget(input1, input2, toolName) {
if (!input1 || !input2) return false;
return compareToolTargets(input1, input2, toolName, "same");
}
function isSimilarInput(input1, input2, toolName) {
if (!input1 && !input2) return true;
if (!input1 || !input2) return false;
if (input1 === input2) return true;
return compareToolTargets(input1, input2, toolName, "similar");
}
function isSameCallTarget(a, b) {
if (!a.input || !b.input) return false;
const toolName = a.name;
const keys = {};
if (toolName === "Edit" || toolName === "Write" || toolName === "Read") {
keys.path1 = a._path !== undefined ? a._path : (a._path = shortToolPath(a.input));
keys.path2 = b._path !== undefined ? b._path : (b._path = shortToolPath(b.input));
} else if (toolName === "Bash") {
keys.bash1 = a._bashFirst !== undefined ? a._bashFirst : (a._bashFirst = bashSplitFirstToken(a.input));
keys.bash2 = b._bashFirst !== undefined ? b._bashFirst : (b._bashFirst = bashSplitFirstToken(b.input));
}
return compareToolTargets(a.input, b.input, toolName, "same", keys);
}
function isSimilarCallEntry(a, b) {
if (!a.input && !b.input) return true;
if (!a.input || !b.input) return false;
if (a.input === b.input) return true;
const toolName = a.name;
const keys = {};
if (toolName === "Edit" || toolName === "Write" || toolName === "Read") {
keys.path1 = a._path !== undefined ? a._path : (a._path = shortToolPath(a.input));
keys.path2 = b._path !== undefined ? b._path : (b._path = shortToolPath(b.input));
} else if (toolName === "Bash") {
keys.bash1 = a._bashTwo !== undefined ? a._bashTwo : (a._bashTwo = bashFirstTwoTokens(a.input));
keys.bash2 = b._bashTwo !== undefined ? b._bashTwo : (b._bashTwo = bashFirstTwoTokens(b.input));
}
return compareToolTargets(a.input, b.input, toolName, "similar", keys);
}
const CORRECTION_HINT_RE = /\b(no|wrong|idiot|revert|undo|actually|instead)\b|not what/i;
function markUserPromptCorrections(chapters) {
for (let i = 0; i < chapters.length - 1; i++) {
const next = chapters[i + 1].prompt;
if (CORRECTION_HINT_RE.test(next)) {
chapters[i].corrected = true;
}
}
}
function countSelfCorrections(seq) {
let count = 0;
for (let i = 0; i < seq.length; i++) {
if (!seq[i].isError) continue;
const errName = seq[i].name;
for (let k = i + 1; k < Math.min(i + 4, seq.length); k++) {
if (seq[k].name === errName) {
if (isSameCallTarget(seq[i], seq[k]) && !seq[k].isError) {
count++;
break;
}
}
}
}
return count;
}
function detectChapterRetries(ch) {
const seq = ch._callSeq;
ch.retries = 0;
ch.retryGroups = [];
let selfCorrections = 0;
if (!seq || seq.length < 2) {
ch.selfCorrections = 0;
return;
}
let i = 0;
while (i < seq.length) {
if (seq[i].isError) {
const errName = seq[i].name;
for (let k = i + 1; k < Math.min(i + 4, seq.length); k++) {
if (seq[k].name === errName) {
if (isSameCallTarget(seq[i], seq[k]) && !seq[k].isError) {
selfCorrections++;
break;
}
}
}
}
let j = i + 1;
const baseName = seq[i].name;
const group = [i];
while (j < seq.length && j - i < 8) {
if (seq[j].name === baseName && isSimilarCallEntry(seq[i], seq[j])) {
group.push(j);
}
j++;
}
if (group.length >= 2) {
ch.retries += group.length - 1;
ch.retryGroups.push({ tool: baseName, count: group.length, input: safeSlice(seq[i].input, 60) });
i = group[group.length - 1] + 1;
} else {
i++;
}
}
ch.selfCorrections = selfCorrections;
}
function classifyChapterOutcome(ch) {
if (ch.selfCorrections === undefined) {
ch.selfCorrections = countSelfCorrections(ch._callSeq || []);
}
if (ch.errors === 0 && ch.retries === 0) {
ch.outcome = "clean";
} else if (ch.selfCorrections > 0 && ch.selfCorrections >= ch.errors * 0.5) {
ch.outcome = "corrected";
} else if (ch.corrected) {
ch.outcome = "corrected";
} else if (ch.retries >= 3 || ch.errors >= 3) {
ch.outcome = "struggling";
} else if (ch.selfCorrections > 0) {
ch.outcome = "corrected";
} else if (ch.errors >= 2 && ch.selfCorrections === 0) {
ch.outcome = "struggling";
} else if (ch.errors === 1 && ch.turns > 1) {
ch.outcome = "corrected";
} else if (ch.retries > 0 && ch.retries < 3) {
ch.outcome = "corrected";
} else if (ch.errors > 0) {
ch.outcome = "corrected";
} else {
ch.outcome = "clean";
}
}
function countGitOpsOfType(gitOps, type) {
let n = 0;
for (let i = 0; i < gitOps.length; i++) {
if (gitOps[i].type === type) n++;
}
return n;
}
function countOkCommands(commands) {
let n = 0;
for (let i = 0; i < commands.length; i++) {
if (commands[i].ok) n++;
}
return n;
}
function computeChapterEfficiency(ch, avgTokPerChapter) {
const chTok = ch.tokens.input + ch.tokens.output;
const fileCount = chapterFileKeys(ch).length;
const commitCount = countGitOpsOfType(ch.gitOps, "commit");
const editWriteCount = (ch.toolCounts.Edit || 0) + (ch.toolCounts.Write || 0);
let callCount = 0;
const toolKeys = chapterToolKeys(ch);
for (let i = 0; i < toolKeys.length; i++) callCount += ch.toolCounts[toolKeys[i]] || 0;
const failedRetries = ch.retries;
const tokPerFile = fileCount > 0 ? Math.round(chTok / fileCount) : 0;
const tokPerCommit = commitCount > 0 ? Math.round(chTok / commitCount) : 0;
const errorTokens = ch.errors > 0 && callCount > 0 ? Math.round(chTok * (ch.errors / callCount)) : 0;
const wasteReasons = [];
const highTokens = chTok > avgTokPerChapter * 1.5 && avgTokPerChapter > 0;
if (
highTokens &&
fileCount === 0 &&
commitCount === 0 &&
editWriteCount === 0 &&
ch.commands.length === 0 &&
ch.agents.length === 0
) {
wasteReasons.push("high tokens, no file changes or actions");
}
if (failedRetries >= 3 && ch.selfCorrections === 0) {
wasteReasons.push(failedRetries + " failed retries without resolution");
}
if (ch.thinking.length > 0 && callCount === 0) {
wasteReasons.push("reasoning with no tool actions");
}
if (ch.errors >= 3 && ch.selfCorrections === 0) {
wasteReasons.push(ch.errors + " errors, no self-correction");
}
if ((ch.toolCounts.Read || 0) >= 6 && editWriteCount === 0 && commitCount === 0 && highTokens) {
wasteReasons.push("heavy reading with no edits");
}
let effScore = 100;
if (callCount > 0) {
effScore -= Math.min(40, (ch.errors / callCount) * 200);
}
if (callCount > 0) {
effScore -= Math.min(25, (ch.retries / callCount) * 150);
}
if (chTok > 0 && avgTokPerChapter > 0) {
const productivity = editWriteCount + commitCount + countOkCommands(ch.commands);
const tokRatio = chTok / avgTokPerChapter;
if (tokRatio > 2 && productivity <= 1) {
effScore -= Math.min(25, (tokRatio - 1) * 10);
}
}
if (ch.selfCorrections > 0 && ch.errors > 0) {
effScore += Math.min(10, (ch.selfCorrections / ch.errors) * 10);
}
effScore = Math.max(0, Math.min(100, Math.round(effScore)));
const isWasteful = wasteReasons.length > 0 || effScore < 40;
ch.efficiency = {
score: effScore,
isWasteful,
wasteReasons,
tokPerFile,
tokPerCommit,
errorTokens,
wasteTokens: isWasteful ? Math.round(chTok * Math.max(0.2, 1 - effScore / 100)) : 0,
};
}
function enrichChaptersQualityCore(chapters) {
for (const ch of chapters) {
detectChapterRetries(ch);
classifyChapterOutcome(ch);
delete ch._callSeq;
}
}
function buildSessionChapters(session) {
const events = session.events || [];
const chapters = [];
let current = null;
const { resultMap, toolNameMap } = buildChapterEventMaps(events);
let compareUserPrompt = null;
let compareAssistantPrompt = null;
for (const e of events) {
if (!compareUserPrompt && e.type === "user" && e.text) compareUserPrompt = e.text;
else if (!compareAssistantPrompt && e.type === "assistant" && e.text && e.text.length > 20) {
compareAssistantPrompt = e.text;
}
if (e.type === "user" && e.text) {
if (current) chapters.push(current);
current = newChapter(e.text, e.timestamp);
continue;
}
if (!current) {
if (e.type === "assistant") {
current = newChapter("(subagent session)", e.timestamp || session.startTime);
} else {
continue;
}
}
if (current.prompt === "(subagent session)" && e.type === "assistant" && e.text && e.text.length > 20) {
current.prompt = e.text;
}
if (e.type === "assistant") {
current.turns++;
current.endTimestamp = e.timestamp;
if (e.text) current.lastAssistantText = e.text;
if (e.tokens) {
current.tokens.input += e.tokens.input || 0;
current.tokens.output += e.tokens.output || 0;
current.tokens.cacheHit += e.tokens.cacheHit || 0;
current.tokens.cacheWrite += e.tokens.cacheWrite || 0;
}
if (e.thinking && current.thinking.length < 20) {
for (const t of e.thinking) {
if (t) current.thinking.push(t);
}
}
for (const tc of e.toolCalls || []) {
accumulateAssistantToolCall(current, tc, resultMap, {
trackCallSeq: true,
searchOutput: true,
agentResult: true,
webFetchPreview: true,
});
}
}
if (e.type === "tool_result" && e.isError) {
current.errors++;
if (toolNameMap[e.toolUseId]) {
current.errorTools[toolNameMap[e.toolUseId]] =
(current.errorTools[toolNameMap[e.toolUseId]] || 0) + 1;
}
}
}
if (current) chapters.push(current);
markUserPromptCorrections(chapters);
for (const ch of chapters) {
ch._fileKeys = Object.keys(ch.files);
ch._toolKeys = Object.keys(ch.toolCounts);
}
enrichChaptersQualityCore(chapters);
const comparePrompt =
safeSlice(compareUserPrompt, 120) || safeSlice(compareAssistantPrompt, 120) || "";
if (comparePrompt) chapters._comparePrompt = comparePrompt;
return chapters;
}
function enrichChaptersEfficiency(chapters) {
let totalTokAll = 0;
for (const ch of chapters) {
totalTokAll += ch.tokens.input + ch.tokens.output;
}
const avgTokPerChapter = chapters.length > 0 ? totalTokAll / chapters.length : 0;
for (const ch of chapters) {
computeChapterEfficiency(ch, avgTokPerChapter);
}
}
function enrichChaptersForRender(chapters) {
const EMPTY_DEPS = { continuesFrom: [], fixesFrom: [], sharedFiles: {} };
function addChapterTokens(charBits, bigrams, extraChars, str) {
if (!str) return;
const len = str.length;
for (let i = 0; i < len; i++) {
const c = str.charCodeAt(i);
if (c < 256) charBits[c >>> 5] |= 1 << (c & 31);
else extraChars.add(str[i]);
if (i + 1 < len) bigrams.add((c << 16) | str.charCodeAt(i + 1));
}
}
function cacheLowerAndToken(charBits, bigrams, extraChars, raw) {
const str = (raw || "").toLowerCase();
addChapterTokens(charBits, bigrams, extraChars, str);
return str;
}
for (let i = 0; i < chapters.length; i++) {
const ch = chapters[i];
const chFiles = chapterFileKeys(ch);
if (chFiles.length === 0 && ch.errors === 0) {
ch.deps = EMPTY_DEPS;
continue;
}
const lookback = Math.max(0, i - 10);
const continuesRefs = new Set();
const fixesRefs = new Set();
const sharedFiles = {};
for (let j = lookback; j < i; j++) {
const prev = chapters[j];
let prevFileSet = prev._fileKeysSet;
if (!prevFileSet) {
prev._fileKeysSet = prevFileSet = new Set(chapterFileKeys(prev));
}
const shared = [];
for (const f of chFiles) {
if (prevFileSet.has(f)) shared.push(f);
}
if (shared.length > 0) {
sharedFiles[j] = shared;
if (
shared.length >= Math.min(2, chFiles.length) ||
shared.length >= chFiles.length * 0.5
) {
continuesRefs.add(j);
}
}
if (prev.outcome === "struggling" || prev.errors >= 2) {
if (shared.length > 0) {
fixesRefs.add(j);
// Remove from continuesFrom to avoid double-labeling
continuesRefs.delete(j);
}
}
}
if (
continuesRefs.size === 0 &&
fixesRefs.size === 0 &&
Object.keys(sharedFiles).length === 0
) {
ch.deps = EMPTY_DEPS;
} else {
ch.deps = {
continuesFrom: Array.from(continuesRefs),
fixesFrom: Array.from(fixesRefs),
sharedFiles,
};
}
}
const chainOf = new Array(chapters.length).fill(-1);
let chainCount = 0;
for (let i = 0; i < chapters.length; i++) {
const deps = chapters[i].deps;
const continuesFrom = deps.continuesFrom;
const fixesFrom = deps.fixesFrom;
if (continuesFrom.length === 0 && fixesFrom.length === 0) continue;
let existingChain = -1;
for (let r = 0; r < continuesFrom.length; r++) {
const ref = continuesFrom[r];
if (chainOf[ref] >= 0) {
existingChain = chainOf[ref];
break;
}
}
if (existingChain < 0) {
for (let r = 0; r < fixesFrom.length; r++) {
const ref = fixesFrom[r];
if (chainOf[ref] >= 0) {
existingChain = chainOf[ref];
break;
}
}
}
if (existingChain >= 0) {
chainOf[i] = existingChain;
} else {
const seed = continuesFrom.length > 0 ? continuesFrom[0] : fixesFrom[0];
chainOf[seed] = chainCount;
chainOf[i] = chainCount;
chainCount++;
}
}
const chainsSet = new Set();
let chaptersInChains = 0;
for (let i = 0; i < chainOf.length; i++) {
if (chainOf[i] >= 0) {
chainsSet.add(chainOf[i]);
chaptersInChains++;
}
}
const workflowChainCount = chainsSet.size;
if (chapters.length > 0) {
chapters._depSummary = { chains: workflowChainCount, chaptersInChains };
}
for (const ch of chapters) {
const charBits = (ch._charBits = new Uint32Array(8));
const bigrams = (ch._tokenBigrams = new Set());
const extraChars = (ch._extraChars = new Set());
ch._promptLower = cacheLowerAndToken(charBits, bigrams, extraChars, ch.prompt);
ch._lastAssistantTextLower = cacheLowerAndToken(
charBits,
bigrams,
extraChars,
ch.lastAssistantText,
);
const fileKeys = chapterFileKeys(ch);
const fileKeysLower = new Array(fileKeys.length);
for (let fi = 0; fi < fileKeys.length; fi++) {
const lower = (fileKeys[fi] || "").toLowerCase();
fileKeysLower[fi] = lower;
addChapterTokens(charBits, bigrams, extraChars, lower);
}
ch._fileKeysLower = fileKeysLower;
for (const cmd of ch.commands) {
cmd._cmdLower = cacheLowerAndToken(charBits, bigrams, extraChars, cmd.cmd);
cmd._outputLower = cacheLowerAndToken(charBits, bigrams, extraChars, cmd.output);
}
for (const s of ch.searches) {
s._queryLower = cacheLowerAndToken(charBits, bigrams, extraChars, s.query);
}
for (const a of ch.agents) {
a._descriptionLower = cacheLowerAndToken(charBits, bigrams, extraChars, a.description);
a._promptLower = cacheLowerAndToken(charBits, bigrams, extraChars, a.prompt);
}
for (const d of ch.diffs) {
d._pathLower = cacheLowerAndToken(charBits, bigrams, extraChars, d.path);
if (d.diffInfo) {
d.diffInfo._oldStrLower = cacheLowerAndToken(charBits, bigrams, extraChars, d.diffInfo.oldStr);
d.diffInfo._newStrLower = cacheLowerAndToken(charBits, bigrams, extraChars, d.diffInfo.newStr);
d.diffInfo._contentLower = cacheLowerAndToken(
charBits,
bigrams,
extraChars,
d.diffInfo.content,
);
}
}
for (const g of ch.gitOps) {
g._messageLower = cacheLowerAndToken(charBits, bigrams, extraChars, g.message);
g._branchLower = cacheLowerAndToken(charBits, bigrams, extraChars, g.branch);
g._hashLower = cacheLowerAndToken(charBits, bigrams, extraChars, g.hash);
g._cmdLower = cacheLowerAndToken(charBits, bigrams, extraChars, g.cmd);
}
for (const w of ch.webOps) {
w._urlLower = cacheLowerAndToken(charBits, bigrams, extraChars, w.url);
w._queryLower = cacheLowerAndToken(charBits, bigrams, extraChars, w.query);
w._pageTitleLower = cacheLowerAndToken(charBits, bigrams, extraChars, w.pageTitle);
if (w.results) {
for (const r of w.results) {
r._titleLower = cacheLowerAndToken(charBits, bigrams, extraChars, r.title);
r._urlLower = cacheLowerAndToken(charBits, bigrams, extraChars, r.url);
}
}
}
ch._thinkingLower = ch.thinking.map((t) => {
const lower = (t || "").toLowerCase();
addChapterTokens(charBits, bigrams, extraChars, lower);
return lower;
});
for (const m of ch.mcpOps) {
m._serverLower = cacheLowerAndToken(charBits, bigrams, extraChars, m.server);
m._toolLower = cacheLowerAndToken(charBits, bigrams, extraChars, m.tool);
m._rawNameLower = cacheLowerAndToken(charBits, bigrams, extraChars, m.rawName);
const params = m.params;
const paramVals = [];
if (params) {
for (const k in params) {
if (Object.prototype.hasOwnProperty.call(params, k)) {
const pv = String(params[k] || "").toLowerCase();
paramVals.push(pv);
addChapterTokens(charBits, bigrams, extraChars, pv);
}
}
}
m._paramValsLower = paramVals;
}
}
}
function buildChapters() {
const chapters = buildSessionChapters({ events, startTime: session.startTime });
enrichChaptersEfficiency(chapters);
enrichChaptersForRender(chapters);
return chapters;
}
let chaptersCache = null;
function getChapters() {
if (!chaptersCache) chaptersCache = buildChapters();
return chaptersCache;
}
let activeToolFilters = new Set();
let searchQuery = '';
let _searchQueryLowerSrc = null;
let _searchQueryLowerCache = '';
let wasteFilterActive = false;
function chapterMatchesFilter(ch) {
if (wasteFilterActive) {
if (!ch.efficiency || !ch.efficiency.isWasteful) return false;
}
if (activeToolFilters.size > 0) {
const hasMatch = chapterToolKeys(ch).some(t => activeToolFilters.has(t));
if (!hasMatch) return false;
}
if (searchQuery) {
if (searchQuery !== _searchQueryLowerSrc) {
_searchQueryLowerSrc = searchQuery;
_searchQueryLowerCache = searchQuery.toLowerCase();
}
const q = _searchQueryLowerCache;
// Early-exit for short queries: if query not in token index, chapter definitely doesn't match
if (q.length <= 2) {
if (q.length === 1) {
const c = q.charCodeAt(0);
if (c < 256) {
if (!(ch._charBits[c >>> 5] & (1 << (c & 31)))) return false;
} else if (!ch._extraChars.has(q)) {
return false;
}
} else {
const key = (q.charCodeAt(0) << 16) | q.charCodeAt(1);
if (!ch._tokenBigrams.has(key)) return false;
}
}
if (ch._promptLower.includes(q)) return true;
if (ch._lastAssistantTextLower.includes(q)) return true;
for (const cmd of ch.commands) {
if (cmd._cmdLower.includes(q)) return true;
if (cmd._outputLower && cmd._outputLower.includes(q)) return true;
}
for (const path of ch._fileKeysLower) {
if (path.includes(q)) return true;
}
for (const s of ch.searches) {
if (s._queryLower.includes(q)) return true;
}
for (const a of ch.agents) {
if (a._descriptionLower.includes(q)) return true;
if (a._promptLower.includes(q)) return true;
}
for (const d of ch.diffs) {
if (d._pathLower.includes(q)) return true;
if (d.diffInfo._oldStr && d.diffInfo._oldStrLower.includes(q)) return true;
if (d.diffInfo._newStr && d.diffInfo._newStrLower.includes(q)) return true;
if (d.diffInfo._content && d.diffInfo._contentLower.includes(q)) return true;
}
for (const g of ch.gitOps) {
if (g._messageLower && g._messageLower.includes(q)) return true;
if (g._branchLower && g._branchLower.includes(q)) return true;
if (g._hashLower && g._hashLower.includes(q)) return true;
if (g._cmdLower && g._cmdLower.includes(q)) return true;
}
for (const w of ch.webOps) {
if (w._urlLower && w._urlLower.includes(q)) return true;
if (w._queryLower && w._queryLower.includes(q)) return true;
if (w._pageTitleLower && w._pageTitleLower.includes(q)) return true;
if (w.results) {
for (const r of w.results) {
if (r._titleLower.includes(q)) return true;
if (r._urlLower.includes(q)) return true;
}
}
}
for (const t of ch._thinkingLower) {
if (t.includes(q)) return true;
}
for (const m of ch.mcpOps) {
if (m._serverLower.includes(q)) return true;
if (m._toolLower.includes(q)) return true;
if (m._rawNameLower.includes(q)) return true;
for (const pv of m._paramValsLower) {
if (pv.includes(q)) return true;
}
}
return false;
}
return true;
}
function renderFilterBar() {
const chapters = getChapters();
const toolSet = {};
let wastefulCount = 0;
for (const ch of chapters) {
if (ch.efficiency && ch.efficiency.isWasteful) wastefulCount++;
for (const t of chapterToolKeys(ch)) {
toolSet[t] = (toolSet[t] || 0) + ch.toolCounts[t];
}
}
const priority = ['Bash', 'Read', 'Edit', 'Write', 'Agent', 'Grep', 'Skill', 'WebSearch', 'WebFetch'];
const sortedByCount = [];
for (const name in toolSet) {
if (Object.prototype.hasOwnProperty.call(toolSet, name)) sortedByCount.push(name);
}
sortedByCount.sort((a, b) => toolSet[b] - toolSet[a]);
const chipNames = [];
for (const p of priority) {
if (toolSet[p]) chipNames.push(p);
}
for (const name of sortedByCount) {
if (!chipNames.includes(name) && chipNames.length < 10) chipNames.push(name);
}
const bar = h('div', { className: 'filter-bar' });
const toolsWrap = h('div', { className: 'filter-tools' });
for (const name of chipNames) {
const isActive = activeToolFilters.has(name);
const chipLabel = fmtMcpName(name);
const chip = h('span', {
className: 'filter-chip' + (isActive ? ' active' : ''),
onClick: (ev) => {
ev.stopPropagation();
if (activeToolFilters.has(name)) activeToolFilters.delete(name);
else activeToolFilters.add(name);
activeToolPerfFilter = null;
var perfRows = document.querySelectorAll('.tool-perf-row');
perfRows.forEach(function(r) { r.classList.remove('active'); });
applyFilters();
}
}, chipLabel);
chip.dataset.tool = name;
toolsWrap.appendChild(chip);
}
if (wastefulCount > 0) {
const wasteChip = h('span', {
className: 'filter-chip' + (wasteFilterActive ? ' active' : ''),
onClick: (ev) => {
ev.stopPropagation();
wasteFilterActive = !wasteFilterActive;
applyFilters();
wasteChip.classList.toggle('active', wasteFilterActive);
},
style: 'border-color: rgba(232,164,76,0.3);'
}, '\u26a0 wasteful ' + wastefulCount);
toolsWrap.appendChild(wasteChip);
}
bar.appendChild(toolsWrap);
const searchInput = h('input', {
className: 'filter-search',
type: 'text',
placeholder: 'search chapters...',
});
searchInput.value = searchQuery;
searchInput.addEventListener('input', function() {
searchQuery = this.value;
applyFilters();
});
bar.appendChild(searchInput);
const countEl = h('span', { className: 'filter-count', id: 'filter-count' });
bar.appendChild(countEl);
return bar;
}
function applyFilters() {
const chapters = getChapters();
let visible = 0;
for (let i = 0; i < chapters.length; i++) {
const el = document.getElementById('chapter-' + i);
if (!el) continue;
const matches = chapterMatchesFilter(chapters[i]);
if (matches) {
el.classList.remove('filter-hidden');
visible++;
} else {
el.classList.add('filter-hidden');
}
}
const countEl = document.getElementById('filter-count');
if (countEl) {
const hasFilter = activeToolFilters.size > 0 || searchQuery || wasteFilterActive;
countEl.textContent = hasFilter ? visible + '/' + chapters.length : '';
}
const chips = document.querySelectorAll('.filter-chip');
chips.forEach(function(chip) {
if (chip.textContent.indexOf('wasteful') !== -1) return;
var toolKey = chip.dataset.tool || chip.textContent;
if (activeToolFilters.has(toolKey)) chip.classList.add('active');
else chip.classList.remove('active');
});
updateUrl();
}
function renderChapters() {
const chapters = getChapters();
const wrap = h('div', { className: 'chapters' });
chapters.forEach((ch, idx) => {
const expanded = expandedSet.has('ch' + idx);
const outcome = ch.outcome || 'clean';
const outcomeCls = outcome === 'struggling' ? 'error' : outcome;
const outcomeLabel = outcome === 'clean' ? 'clean'
: outcome === 'corrected' ? 'corrected'
: outcome === 'struggling' ? 'struggling' : 'clean';
const el = h('div', {
id: 'chapter-' + idx,
className: 'chapter' + (expanded ? ' expanded' : ''),
onClick: () => {
if (expanded) {
expandedSet.delete('ch' + idx);
clearChapterHash();
} else {
expandedSet.add('ch' + idx);
setChapterHash(idx);
}
render();
}
});
const toolChips = h('div', { className: 'chapter-tools' });
const sortedTools = chapterToolKeys(ch).slice()
.sort((a, b) => ch.toolCounts[b] - ch.toolCounts[a]);
for (const name of sortedTools) {
const count = ch.toolCounts[name];
const cls = ['Bash','Read','Edit','Write','Agent'].includes(name) ? name : 'other';
const chipLabel = fmtMcpName(name);
toolChips.appendChild(h('span', { className: 'ch-tool ' + cls }, chipLabel + ' ' + count));
}
const startTime = ch.timestamp ? formatTime(ch.timestamp) : '';
const endMs = new Date(ch.endTimestamp).getTime();
const startMs = new Date(ch.timestamp).getTime();
const dur = endMs - startMs;
const patternBadges = [];
if (ch.retries > 0) {
patternBadges.push(h('span', { className: 'chapter-pattern-badge retry' },
'\u21bb ' + ch.retries + ' retr' + (ch.retries === 1 ? 'y' : 'ies')));
}
if (ch.selfCorrections > 0) {
patternBadges.push(h('span', { className: 'chapter-pattern-badge correction' },
'\u2714 ' + ch.selfCorrections + ' self-fix' + (ch.selfCorrections === 1 ? '' : 'es')));
}
if (ch.efficiency && ch.efficiency.isWasteful) {
patternBadges.push(h('span', { className: 'chapter-waste-badge' },
'\u26a0 waste' + (ch.efficiency.wasteTokens > 0 ? ' ~' + fmtTokens(ch.efficiency.wasteTokens) : '')));
}
if (ch.deps) {
if (ch.deps.fixesFrom.length > 0) {
var fixRef = ch.deps.fixesFrom[0];
var badge = h('span', {
className: 'chapter-dep-badge dep-fix',
title: 'Fixes issues from chapter ' + (fixRef + 1),
onClick: function(ev) {
ev.stopPropagation();
var target = document.getElementById('chapter-' + fixRef);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, '\u2192 fixes ch ' + (fixRef + 1));
patternBadges.push(badge);
} else if (ch.deps.continuesFrom.length > 0) {
var contRef = ch.deps.continuesFrom[ch.deps.continuesFrom.length - 1];
var badge = h('span', {
className: 'chapter-dep-badge',
title: 'Continues work from chapter ' + (contRef + 1),
onClick: function(ev) {
ev.stopPropagation();
var target = document.getElementById('chapter-' + contRef);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, '\u2192 from ch ' + (contRef + 1));
patternBadges.push(badge);
}
}
const dotCls = 'chapter-outcome-dot dot-' + outcome;
const head = h('div', { className: 'chapter-head' },
h('span', { className: 'chapter-num' },
String(idx + 1),
h('span', { className: dotCls })
),
h('div', { className: 'chapter-body' },
h('div', { className: 'chapter-prompt' },
h('span', { className: 'chapter-prompt-text' }, ch.prompt)
),
h('div', { className: 'chapter-meta' },
toolChips,
ch.gitOps.length > 0
? (function() {
var commits = countGitOpsOfType(ch.gitOps, 'commit');
var label = commits > 0 ? '● ' + commits + ' commit' + (commits > 1 ? 's' : '') : '⑃ ' + ch.gitOps.length + ' git op' + (ch.gitOps.length > 1 ? 's' : '');
return h('span', { className: 'chapter-git-badge' }, label);
})()
: null,
ch.mcpOps.length > 0
? (function() {
var servers = {};
for (var mci = 0; mci < ch.mcpOps.length; mci++) {
var sv = ch.mcpOps[mci].server;
servers[sv] = (servers[sv] || 0) + 1;
}
var svNames = Object.keys(servers);
var label = svNames.length === 1
? svNames[0].replace(/_/g, ' ') + ' \u00d7' + ch.mcpOps.length
: ch.mcpOps.length + ' MCP';
return h('span', { className: 'chapter-mcp-badge' }, '\u2699 ' + label);
})()
: null,
patternBadges.length > 0
? (function() {
var wrap = h('span', { className: 'chapter-patterns' });
patternBadges.forEach(function(b) { wrap.appendChild(b); });
return wrap;
})()
: null,
h('span', { className: 'chapter-outcome ' + outcomeCls }, outcomeLabel),
)
),
h('div', { className: 'chapter-right' },
h('div', { className: 'chapter-time' }, startTime),
h('div', { className: 'chapter-turns' },
ch.turns + ' turn' + (ch.turns !== 1 ? 's' : '') +
(dur > 1000 ? ' · ' + formatDuration(dur) : '')
),
(ch.tokens.input > 0 || ch.tokens.output > 0)
? h('div', { className: 'chapter-tokens' },
fmtTokens(ch.tokens.input + ch.tokens.output) + ' tok' +
(ch.tokens.cacheHit > 0 && ch.tokens.input > 0 ? ' · ' + fmtPct(ch.tokens.cacheHit / ch.tokens.input * 100) + ' cache' : '')
)
: null,
ch.thinking.length > 0
? h('div', { className: 'chapter-thinking-badge' },
'🧠 ' + countWordsInStrings(ch.thinking).toLocaleString() + ' words'
)
: null,
(function() {
var link = h('span', {
className: 'chapter-permalink',
title: 'Copy link to this chapter',
onClick: function(ev) {
ev.stopPropagation();
var url = getPermalink(idx);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(function() {
link.textContent = '\u2713';
link.classList.add('copied');
setTimeout(function() { link.textContent = '\u26d3'; link.classList.remove('copied'); }, 1500);
});
} else {
var tmp = document.createElement('input');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
link.textContent = '\u2713';
link.classList.add('copied');
setTimeout(function() { link.textContent = '\u26d3'; link.classList.remove('copied'); }, 1500);
}
}
}, '\u26d3');
return link;
})(),
)
);
el.appendChild(head);
const detail = h('div', { className: 'chapter-detail' });
if (expanded) {
buildChapterDetail(ch, idx, detail);
}
el.appendChild(detail);
wrap.appendChild(el);
});
return wrap;
}
function buildChapterDetail(ch, _idx, detail) {
var outcome = ch.outcome || 'clean';
if (ch.retries > 0 || ch.selfCorrections > 0) {
var qDiv = h('div', { className: 'chapter-quality' });
var qLabel = outcome === 'struggling' ? 'struggling'
: outcome === 'corrected' ? 'self-corrected' : 'minor issues';
qDiv.appendChild(h('div', { className: 'chapter-quality-label ' + outcome }, qLabel));
if (ch.retryGroups.length > 0) {
var retryDiv = h('div', { className: 'chapter-quality-retries' });
for (var ri = 0; ri < ch.retryGroups.length && ri < 4; ri++) {
var rg = ch.retryGroups[ri];
retryDiv.appendChild(h('span', { className: 'chapter-retry-chip' },
rg.tool + ' \u00d7' + rg.count + (rg.input ? ' (' + rg.input.slice(0, 30) + (rg.input.length > 30 ? '...' : '') + ')' : '')));
}
qDiv.appendChild(retryDiv);
}
if (ch.selfCorrections > 0) {
qDiv.appendChild(h('div', { className: 'chapter-quality-corrections' },
'\u2714 ' + ch.selfCorrections + ' error' + (ch.selfCorrections === 1 ? '' : 's') + ' detected and resolved'));
}
detail.appendChild(qDiv);
}
if (ch.deps && (ch.deps.continuesFrom.length > 0 || ch.deps.fixesFrom.length > 0)) {
var depDiv = h('div', { className: 'chapter-dep-section' });
depDiv.appendChild(h('div', { className: 'chapter-dep-section-title' }, 'related chapters'));
if (ch.deps.fixesFrom.length > 0) {
for (var fi = 0; fi < ch.deps.fixesFrom.length; fi++) {
var ref = ch.deps.fixesFrom[fi];
var row = document.createElement('div');
row.style.marginBottom = '3px';
var link = h('span', {
className: 'chapter-dep-link',
onClick: (function(r) { return function(ev) {
ev.stopPropagation();
var target = document.getElementById('chapter-' + r);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}; })(ref)
}, '\u2714 fixes ch ' + (ref + 1));
row.appendChild(link);
var shared = ch.deps.sharedFiles[ref] || [];
if (shared.length > 0) {
row.appendChild(h('span', { className: 'chapter-dep-shared-files' },
'(' + shared.slice(0, 3).join(', ') + (shared.length > 3 ? ' +' + (shared.length - 3) : '') + ')'));
}
depDiv.appendChild(row);
}
}
if (ch.deps.continuesFrom.length > 0) {
for (var ci2 = 0; ci2 < ch.deps.continuesFrom.length; ci2++) {
var ref2 = ch.deps.continuesFrom[ci2];
var row2 = document.createElement('div');
row2.style.marginBottom = '3px';
var link2 = h('span', {
className: 'chapter-dep-link',
onClick: (function(r) { return function(ev) {
ev.stopPropagation();
var target = document.getElementById('chapter-' + r);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}; })(ref2)
}, '\u2192 continues ch ' + (ref2 + 1));
row2.appendChild(link2);
var shared2 = ch.deps.sharedFiles[ref2] || [];
if (shared2.length > 0) {
row2.appendChild(h('span', { className: 'chapter-dep-shared-files' },
'(' + shared2.slice(0, 3).join(', ') + (shared2.length > 3 ? ' +' + (shared2.length - 3) : '') + ')'));
}
depDiv.appendChild(row2);
}
}
detail.appendChild(depDiv);
}
if (ch.efficiency && (ch.efficiency.isWasteful || ch.efficiency.score < 70)) {
var effDiv = h('div', { className: 'chapter-efficiency' });
var effHeader = h('div', { className: 'chapter-efficiency-header' });
var scoreLabel = ch.efficiency.score >= 70 ? 'moderate efficiency'
: ch.efficiency.score >= 40 ? 'low efficiency' : 'wasteful';
effHeader.appendChild(h('span', { className: 'chapter-efficiency-label' }, scoreLabel));
effHeader.appendChild(h('span', { className: 'chapter-efficiency-score' }, ch.efficiency.score + '/100'));
effDiv.appendChild(effHeader);
var barWrap = h('div', { className: 'chapter-efficiency-bar-wrap' });
var barCls = ch.efficiency.score >= 70 ? 'eff-good' : ch.efficiency.score >= 40 ? 'eff-ok' : 'eff-bad';
var bar = h('div', { className: 'chapter-efficiency-bar ' + barCls });
bar.style.width = Math.max(2, ch.efficiency.score) + '%';
barWrap.appendChild(bar);
effDiv.appendChild(barWrap);
var metricsDiv = h('div', { className: 'chapter-efficiency-metrics' });
if (ch.efficiency.tokPerFile > 0) {
var valCls = ch.efficiency.tokPerFile > 500000 ? 'val-bad' : '';
metricsDiv.appendChild(h('span', { className: 'chapter-efficiency-metric' },
'tokens/file: ',
h('span', { className: 'chapter-efficiency-metric-value ' + valCls }, fmtTokens(ch.efficiency.tokPerFile))
));
}
if (ch.efficiency.tokPerCommit > 0) {
var valCls2 = ch.efficiency.tokPerCommit > 1000000 ? 'val-bad' : '';
metricsDiv.appendChild(h('span', { className: 'chapter-efficiency-metric' },
'tokens/commit: ',
h('span', { className: 'chapter-efficiency-metric-value ' + valCls2 }, fmtTokens(ch.efficiency.tokPerCommit))
));
}
if (ch.efficiency.errorTokens > 0) {
metricsDiv.appendChild(h('span', { className: 'chapter-efficiency-metric' },
'error tokens: ',
h('span', { className: 'chapter-efficiency-metric-value val-bad' }, '~' + fmtTokens(ch.efficiency.errorTokens))
));
}
if (ch.efficiency.wasteTokens > 0) {
metricsDiv.appendChild(h('span', { className: 'chapter-efficiency-metric' },
'est. wasted: ',
h('span', { className: 'chapter-efficiency-metric-value val-bad' }, '~' + fmtTokens(ch.efficiency.wasteTokens))
));
}
if (metricsDiv.childNodes.length > 0) effDiv.appendChild(metricsDiv);
if (ch.efficiency.wasteReasons.length > 0) {
var reasonsDiv = h('div', { className: 'chapter-efficiency-reasons' });
for (var wr = 0; wr < ch.efficiency.wasteReasons.length; wr++) {
reasonsDiv.appendChild(h('span', { className: 'chapter-efficiency-reason' }, ch.efficiency.wasteReasons[wr]));
}
effDiv.appendChild(reasonsDiv);
}
detail.appendChild(effDiv);
}
var fileKeys = chapterFileKeys(ch);
if (fileKeys.length) {
var filesDiv = h('div', { className: 'chapter-files' });
filesDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'files'));
for (var fei = 0; fei < fileKeys.length; fei++) {
var path = fileKeys[fei];
var info = ch.files[path];
var opCount = {};
info.ops.forEach(function(o) { opCount[o] = (opCount[o] || 0) + 1; });
var opStr = '';
var opFirst = true;
for (var op in opCount) {
if (!Object.prototype.hasOwnProperty.call(opCount, op)) continue;
if (!opFirst) opStr += ', ';
opFirst = false;
var opN = opCount[op];
opStr += op.toLowerCase() + (opN > 1 ? ' \u00d7' + opN : '');
}
filesDiv.appendChild(h('div', { className: 'chapter-file' },
path,
h('span', { className: 'chapter-file-ops' }, opStr)
));
if (info.output) {
filesDiv.appendChild(h('div', { className: 'chapter-output' }, info.output));
}
}
detail.appendChild(filesDiv);
}
if (ch.gitOps.length > 0) {
var gitDiv = h('div', { className: 'chapter-git-ops' });
gitDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'git'));
for (var gi = 0; gi < ch.gitOps.length; gi++) {
var op = ch.gitOps[gi];
var iconCls = 'chapter-git-op-icon ';
var icon = '';
if (op.type === 'commit') { iconCls += 'commit'; icon = '\u25cf'; }
else if (op.type === 'push') { iconCls += 'push'; icon = '\u2191'; }
else if (op.type === 'branch-create' || op.type === 'branch-switch') { iconCls += 'branch'; icon = '\u2443'; }
else if (op.type === 'merge' || op.type === 'rebase') { iconCls += 'merge'; icon = '\u2442'; }
else { icon = '\u00b7'; }
var desc = '';
if (op.type === 'commit') {
desc = (op.hash ? op.hash.slice(0, 7) + ' ' : '') + (op.message || '(no message)');
} else if (op.type === 'push') {
desc = 'push' + (op.remote ? ' ' + op.remote : '') + (op.branch ? ' ' + op.branch : '') + (op.tags ? ' --tags' : '');
} else if (op.type === 'branch-create') {
desc = 'create branch ' + (op.branch || '');
} else if (op.type === 'branch-switch') {
desc = 'switch to ' + (op.branch || '');
} else if (op.type === 'merge') {
desc = 'merge ' + (op.branch || '');
} else if (op.type === 'rebase') {
desc = 'rebase onto ' + (op.branch || '');
} else if (op.type === 'tag') {
desc = 'tag ' + (op.tag || '');
} else if (op.type === 'stash') {
desc = 'stash';
} else {
desc = op.type;
}
gitDiv.appendChild(h('div', { className: 'chapter-git-op' },
h('span', { className: iconCls }, icon),
desc
));
}
detail.appendChild(gitDiv);
}
if (ch.agents.length) {
var agentsDiv = h('div', { className: 'chapter-agents' });
agentsDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'subagents'));
for (var ai = 0; ai < ch.agents.length; ai++) {
var a = ch.agents[ai];
var statusCls = a.isError ? 'fail' : (a.completed ? 'ok' : 'pending');
var statusIcon = a.isError ? '\u2717' : (a.completed ? '\u2713' : '\u25e6');
var typeBadge = a.subagentType ? h('span', { className: 'chapter-agent-type' }, a.subagentType) : null;
var toolBadge = a.toolName !== 'Agent' ? h('span', { className: 'chapter-agent-type' }, a.toolName) : null;
agentsDiv.appendChild(h('div', { className: 'chapter-agent' },
h('span', { className: 'chapter-agent-status ' + statusCls }, statusIcon),
h('span', { className: 'chapter-agent-desc' }, a.description || '(unnamed)'),
typeBadge,
toolBadge
));
if (a.prompt) {
agentsDiv.appendChild(h('div', { className: 'chapter-agent-prompt' }, a.prompt.slice(0, 200)));
}
if (a.result && a.completed) {
agentsDiv.appendChild(h('div', { className: 'chapter-output' }, joinFirstLines(a.result, 4)));
}
}
detail.appendChild(agentsDiv);
}
if (ch.diffs.length) {
var diffsDiv = h('div', { className: 'chapter-diffs' });
diffsDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'changes'));
for (var di = 0; di < ch.diffs.length && di < 6; di++) {
var d = ch.diffs[di];
var label = d.name === 'Write' ? 'write' : 'edit';
diffsDiv.appendChild(h('div', { className: 'chapter-diff-header' },
h('span', { className: 'chapter-diff-path' }, d.path),
h('span', { className: 'chapter-diff-op' }, label)
));
if (d.name === 'Edit' && d.diffInfo.oldStr) {
var diffBlock = h('div', { className: 'chapter-diff-block' });
diffBlock.appendChild(h('div', { className: 'chapter-diff-del' }, d.diffInfo.oldStr));
if (d.diffInfo.newStr) {
diffBlock.appendChild(h('div', { className: 'chapter-diff-add' }, d.diffInfo.newStr));
}
diffsDiv.appendChild(diffBlock);
} else if (d.name === 'Write' && d.diffInfo.content) {
diffsDiv.appendChild(h('div', { className: 'chapter-diff-block' },
h('div', { className: 'chapter-diff-add' }, joinFirstLines(d.diffInfo.content, 6))
));
}
}
if (ch.diffs.length > 6) {
diffsDiv.appendChild(h('div', { className: 'chapter-diff-header' },
h('span', { className: 'chapter-diff-op' }, '\u2026 ' + (ch.diffs.length - 6) + ' more changes')
));
}
detail.appendChild(diffsDiv);
}
if (ch.searches.length) {
var searchDiv = h('div', { className: 'chapter-commands' });
searchDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'searches'));
for (var si = 0; si < ch.searches.length && si < 6; si++) {
var s = ch.searches[si];
var sLabel = s.matches > 0 ? s.matches + ' match' + (s.matches !== 1 ? 'es' : '') : (s.ok ? 'no matches' : 'error');
searchDiv.appendChild(h('div', { className: 'chapter-cmd' },
h('span', { className: 'chapter-cmd-status ' + (s.ok ? 'ok' : 'fail') }, s.ok ? '\u2713' : '\u2717'),
s.query.slice(0, 100),
h('span', { className: 'chapter-search-count' }, sLabel)
));
if (s.output) {
searchDiv.appendChild(h('div', { className: 'chapter-output' }, joinFirstLines(s.output, 3)));
}
}
detail.appendChild(searchDiv);
}
if (ch.webOps.length) {
var webDiv = h('div', { className: 'chapter-web' });
webDiv.appendChild(h('div', { className: 'chapter-web-label' }, 'web'));
for (var wi = 0; wi < ch.webOps.length && wi < 6; wi++) {
var w = ch.webOps[wi];
var opDiv = h('div', { className: 'chapter-web-op' + (w.ok ? '' : ' error') });
var wTypeBadge = h('span', { className: 'chapter-web-type ' + w.type }, w.type);
if (w.type === 'fetch') {
var headerRow = h('div', {});
headerRow.appendChild(wTypeBadge);
if (w.url) {
var wLink = h('a', { className: 'chapter-web-url', href: w.url, target: '_blank', rel: 'noopener' }, w.url.slice(0, 80) + (w.url.length > 80 ? '...' : ''));
wLink.addEventListener('click', function(ev) { ev.stopPropagation(); });
headerRow.appendChild(wLink);
}
opDiv.appendChild(headerRow);
if (w.pageTitle) {
opDiv.appendChild(h('div', { className: 'chapter-web-title' }, w.pageTitle));
}
if (w.prompt) {
opDiv.appendChild(h('div', { className: 'chapter-web-prompt' }, w.prompt));
}
if (w.preview) {
opDiv.appendChild(h('div', { className: 'chapter-web-preview' }, w.preview));
}
} else if (w.type === 'search') {
var headerRow2 = h('div', {});
headerRow2.appendChild(wTypeBadge);
headerRow2.appendChild(h('span', { className: 'chapter-web-query' }, w.query.slice(0, 100)));
if (w.resultCount) {
headerRow2.appendChild(h('span', { className: 'chapter-web-count' }, w.resultCount + ' result' + (w.resultCount !== 1 ? 's' : '')));
}
opDiv.appendChild(headerRow2);
if (w.results && w.results.length) {
var resultsDiv = h('div', { className: 'chapter-web-results' });
for (var wri = 0; wri < w.results.length && wri < 3; wri++) {
var r = w.results[wri];
var resultRow = h('div', { className: 'chapter-web-result' });
resultRow.appendChild(h('span', { className: 'chapter-web-result-title' }, r.title));
if (r.url) {
var rLink = h('a', { className: 'chapter-web-result-url', href: r.url, target: '_blank', rel: 'noopener' }, r.url.replace(/^https?:\/\//, '').slice(0, 40));
rLink.addEventListener('click', function(ev) { ev.stopPropagation(); });
resultRow.appendChild(rLink);
}
resultsDiv.appendChild(resultRow);
}
opDiv.appendChild(resultsDiv);
}
}
webDiv.appendChild(opDiv);
}
if (ch.webOps.length > 6) {
webDiv.appendChild(h('div', { className: 'chapter-web-op' },
h('span', { className: 'chapter-web-type fetch' }, '... ' + (ch.webOps.length - 6) + ' more')
));
}
detail.appendChild(webDiv);
}
if (ch.mcpOps.length) {
var mcpDiv = h('div', { className: 'chapter-mcp' });
mcpDiv.appendChild(h('div', { className: 'chapter-mcp-label' }, 'integrations'));
for (var mi = 0; mi < ch.mcpOps.length && mi < 10; mi++) {
var mop = ch.mcpOps[mi];
var mcpCard = h('div', { className: 'chapter-mcp-op' + (mop.ok ? '' : ' mcp-error') });
var mcpHeader = h('div', { className: 'chapter-mcp-header' });
mcpHeader.appendChild(h('span', { className: 'chapter-mcp-server' }, mop.server.replace(/_/g, ' ')));
mcpHeader.appendChild(h('span', { className: 'chapter-mcp-tool' }, mop.tool.replace(/_/g, ' ')));
mcpHeader.appendChild(h('span', { className: 'chapter-mcp-status ' + (mop.ok ? 'ok' : 'fail') }, mop.ok ? '\u2713' : '\u2717'));
mcpCard.appendChild(mcpHeader);
var paramKeys = Object.keys(mop.params);
if (paramKeys.length) {
var paramsDiv = h('div', { className: 'chapter-mcp-params' });
for (var pk = 0; pk < paramKeys.length && pk < 4; pk++) {
var pKey = paramKeys[pk];
var pVal = mop.params[pKey];
paramsDiv.appendChild(h('span', {}, h('span', { className: 'chapter-mcp-param-key' }, pKey + ': '), h('span', { className: 'chapter-mcp-param-val' }, (pVal || '').slice(0, 120))));
}
mcpCard.appendChild(paramsDiv);
}
if (mop.output) {
var mcpOut = safeSlice(joinFirstLines(mop.output, 4), 300);
mcpCard.appendChild(h('div', { className: 'chapter-mcp-output' }, mcpOut));
}
mcpDiv.appendChild(mcpCard);
}
if (ch.mcpOps.length > 10) {
mcpDiv.appendChild(h('div', { className: 'chapter-mcp-op' },
h('span', { className: 'chapter-mcp-tool' }, '\u2026 ' + (ch.mcpOps.length - 10) + ' more')
));
}
detail.appendChild(mcpDiv);
}
var cmds = ch.commands.slice(0, 8);
if (cmds.length) {
var cmdsDiv = h('div', { className: 'chapter-commands' });
cmdsDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'commands'));
for (var cmi = 0; cmi < cmds.length; cmi++) {
var cmd = cmds[cmi];
cmdsDiv.appendChild(h('div', { className: 'chapter-cmd' },
h('span', { className: 'chapter-cmd-status ' + (cmd.ok ? 'ok' : 'fail') }, cmd.ok ? '\u2713' : '\u2717'),
cmd.cmd.slice(0, 120)
));
if (cmd.output) {
cmdsDiv.appendChild(h('div', { className: 'chapter-output' }, joinFirstLines(cmd.output, 4)));
}
}
if (ch.commands.length > 8) {
cmdsDiv.appendChild(h('div', { className: 'chapter-cmd' },
h('span', { className: 'chapter-cmd-status ok' }, ''),
'\u2026 ' + (ch.commands.length - 8) + ' more'
));
}
detail.appendChild(cmdsDiv);
}
if (ch.tokens.input > 0 || ch.tokens.output > 0) {
var tokDiv = h('div', { className: 'chapter-token-detail' });
tokDiv.appendChild(h('div', { className: 'chapter-files-label' }, 'tokens'));
var parts = [
'input: ' + fmtTokens(ch.tokens.input),
'output: ' + fmtTokens(ch.tokens.output),
];
if (ch.tokens.cacheHit > 0) parts.push('cache read: ' + fmtTokens(ch.tokens.cacheHit));
if (ch.tokens.cacheWrite > 0) parts.push('cache write: ' + fmtTokens(ch.tokens.cacheWrite));
var total = ch.tokens.input + ch.tokens.output;
parts.push('total: ' + fmtTokens(total));
tokDiv.appendChild(h('div', { className: 'chapter-token-breakdown' }, parts.join(' \u00b7 ')));
var cost = estimateChapterTokenCost(session.model, ch.tokens);
const costLabel = fmtCost(cost, { min: 0.001 });
if (costLabel) {
tokDiv.appendChild(h('div', { className: 'chapter-token-cost' }, costLabel));
}
detail.appendChild(tokDiv);
}
if (ch.thinking.length) {
var thinkDiv = h('div', { className: 'chapter-thinking' });
var totalWords = countWordsInStrings(ch.thinking);
var countLabel = ch.thinking.length === 1
? totalWords.toLocaleString() + ' words'
: ch.thinking.length + ' blocks \u00b7 ' + totalWords.toLocaleString() + ' words';
thinkDiv.appendChild(h('div', { className: 'chapter-thinking-header' },
h('span', { className: 'chapter-thinking-icon' }, '\u{1F9E0}'),
h('span', { className: 'chapter-files-label' }, 'thinking'),
h('span', { className: 'chapter-thinking-count' }, countLabel)
));
for (var ti = 0; ti < ch.thinking.length && ti < 3; ti++) {
var preview = safeSlice(joinFirstLines(ch.thinking[ti], 5), 400);
thinkDiv.appendChild(h('div', { className: 'chapter-thinking-block' }, preview));
}
if (ch.thinking.length > 3) {
thinkDiv.appendChild(h('div', { className: 'chapter-thinking-more' }, '\u2026 ' + (ch.thinking.length - 3) + ' more thinking blocks'));
}
detail.appendChild(thinkDiv);
}
if (ch.lastAssistantText) {
detail.appendChild(h('div', { className: 'chapter-response' }, ch.lastAssistantText.slice(0, 500)));
}
}
function expandAllChapterDetails() {
var chapters = getChapters();
for (var i = 0; i < chapters.length; i++) {
var el = document.getElementById('chapter-' + i);
if (!el) continue;
var detail = el.querySelector('.chapter-detail');
if (detail && detail.childNodes.length === 0) {
buildChapterDetail(chapters[i], i, detail);
}
}
attachExpandToggles();
}
window.addEventListener('beforeprint', function() {
expandAllChapterDetails();
});
function drawWaveform(canvas, turns, gaps, userMsgs, maxOut, maxIn, height) {
const HEIGHT = height;
const MID = HEIGHT * 0.5;
const W = canvas.parentElement?.clientWidth || 800;
const ctx = setupHiDpiCanvas(canvas, W, HEIGHT);
const colW = W / turns.length;
const outRange = MID - 8;
const inRange = MID - 8;
ctx.fillStyle = '#111113';
ctx.fillRect(0, 0, W, HEIGHT);
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, MID);
ctx.lineTo(W, MID);
ctx.stroke();
for (let i = 0; i < turns.length; i++) {
const turn = turns[i];
const x = i * colW;
const color = turn.hasError ? TOOL_COLORS._error : getToolColor(turn.tool);
const alpha = 0.35 + 0.65 * (1 - turn.cacheRatio);
const outH = Math.max(2, (turn.output / maxOut) * outRange);
const inH = Math.max(2, (turn.input / maxIn) * inRange);
const outGrad = ctx.createLinearGradient(0, MID - outH, 0, MID);
outGrad.addColorStop(0, color);
outGrad.addColorStop(1, color + '20');
ctx.globalAlpha = alpha;
ctx.fillStyle = outGrad;
ctx.fillRect(x + 0.25, MID - outH, colW - 0.5, outH);
ctx.fillStyle = color;
ctx.globalAlpha = Math.min(1, alpha + 0.3);
ctx.fillRect(x + 0.25, MID - outH, colW - 0.5, Math.min(2, outH));
const inGrad = ctx.createLinearGradient(0, MID, 0, MID + inH);
inGrad.addColorStop(0, color + '40');
inGrad.addColorStop(1, color + '08');
ctx.globalAlpha = alpha * 0.6;
ctx.fillStyle = inGrad;
ctx.fillRect(x + 0.25, MID + 1, colW - 0.5, inH);
if (turn.hasError) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = TOOL_COLORS._error;
ctx.shadowColor = TOOL_COLORS._error;
ctx.shadowBlur = 10;
ctx.fillRect(x, MID - outH - 4, colW, 4);
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(x + colW / 2, HEIGHT - 14, 4, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
}
ctx.globalAlpha = 1;
for (const gap of gaps) {
const x = gap.idx * colW;
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(x, 8);
ctx.lineTo(x, HEIGHT - 8);
ctx.stroke();
ctx.setLineDash([]);
const label = formatDuration(gap.delta);
ctx.font = '9px ' + getComputedStyle(document.body).fontFamily;
ctx.fillStyle = '#5c5c63';
ctx.textAlign = 'center';
ctx.fillText(label, x, HEIGHT - 2);
}
for (const um of userMsgs) {
const x = Math.min(um.idx, turns.length - 1) * colW + colW / 2;
ctx.fillStyle = TOOL_COLORS._user;
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.moveTo(x, 1);
ctx.lineTo(x + 4, 6);
ctx.lineTo(x, 11);
ctx.lineTo(x - 4, 6);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.globalAlpha = 1;
ctx.setLineDash([]);
ctx.strokeStyle = 'rgba(139, 124, 246, 0.18)';
ctx.lineWidth = 1;
for (let c = 1; c < userMsgs.length; c++) {
const x = Math.min(userMsgs[c].idx, turns.length) * colW;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, HEIGHT);
ctx.stroke();
}
return W;
}
function drawChart(canvas, chapterCosts, maxCumulative, maxSingle, height) {
const HEIGHT = height;
const W = canvas.parentElement?.clientWidth || 800;
const ctx = setupHiDpiCanvas(canvas, W, HEIGHT);
const PAD_TOP = 10;
const PAD_BOTTOM = 4;
const chartH = HEIGHT - PAD_TOP - PAD_BOTTOM;
const numCh = chapterCosts.length;
const barW = W / numCh;
ctx.fillStyle = '#111113';
ctx.fillRect(0, 0, W, HEIGHT);
if (maxSingle > 0) {
for (let i = 0; i < numCh; i++) {
const c = chapterCosts[i];
const barH = Math.max(1, (c.cost / maxSingle) * (chartH * 0.4));
const x = i * barW;
const y = HEIGHT - PAD_BOTTOM - barH;
ctx.fillStyle = 'rgba(232, 164, 76, 0.15)';
ctx.fillRect(x + 1, y, barW - 2, barH);
}
}
if (maxCumulative > 0) {
ctx.beginPath();
ctx.moveTo(0, HEIGHT - PAD_BOTTOM);
for (let i = 0; i < numCh; i++) {
const x = i * barW + barW / 2;
const y = PAD_TOP + chartH - (chapterCosts[i].cumulative / maxCumulative) * chartH;
if (i === 0) ctx.lineTo(x, y);
else ctx.lineTo(x, y);
}
ctx.lineTo((numCh - 1) * barW + barW / 2, HEIGHT - PAD_BOTTOM);
ctx.lineTo(0, HEIGHT - PAD_BOTTOM);
ctx.closePath();
const grad = ctx.createLinearGradient(0, PAD_TOP, 0, HEIGHT - PAD_BOTTOM);
grad.addColorStop(0, 'rgba(232, 164, 76, 0.4)');
grad.addColorStop(1, 'rgba(232, 164, 76, 0.02)');
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
for (let i = 0; i < numCh; i++) {
const x = i * barW + barW / 2;
const y = PAD_TOP + chartH - (chapterCosts[i].cumulative / maxCumulative) * chartH;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = 'rgba(232, 164, 76, 0.9)';
ctx.lineWidth = 1.5;
ctx.stroke();
if (numCh <= 50) {
for (let i = 0; i < numCh; i++) {
const x = i * barW + barW / 2;
const y = PAD_TOP + chartH - (chapterCosts[i].cumulative / maxCumulative) * chartH;
ctx.beginPath();
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(232, 164, 76, 0.9)';
ctx.fill();
}
}
}
ctx.fillStyle = '#5c5c63';
ctx.font = '9px ' + getComputedStyle(document.body).fontFamily;
ctx.textAlign = 'left';
const topLabel = fmtCost(maxCumulative, { prefix: '$' });
ctx.fillText(topLabel, 4, PAD_TOP + 8);
ctx.fillText('$0', 4, HEIGHT - PAD_BOTTOM - 2);
return W;
}
function sortToolPerfEntries(entries, mode) {
if (mode === 'errors') {
return entries.slice().sort(function(a, b) {
var errDiff = b[1].errors - a[1].errors;
if (errDiff !== 0) return errDiff;
return b[1].total - a[1].total;
});
}
return entries.slice().sort(function(a, b) { return b[1].total - a[1].total; });
}
function buildToolPerfRow(tool, stats) {
var color = getToolColor(tool);
var successes = stats.total - stats.errors;
var rate = stats.total > 0 ? (successes / stats.total * 100) : 100;
var rateStr = fmtPct(rate);
var rateCls = 'tool-perf-rate';
if (rate === 100) rateCls += ' perfect';
else if (rate >= 90) rateCls += ' good';
else if (rate >= 70) rateCls += ' warn';
else rateCls += ' bad';
var isActive = activeToolPerfFilter === tool;
var row = h('div', { className: 'tool-perf-row' + (isActive ? ' active' : '') });
row.addEventListener('click', function(ev) {
ev.stopPropagation();
if (activeToolPerfFilter === tool) {
activeToolPerfFilter = null;
activeToolFilters.clear();
} else {
activeToolPerfFilter = tool;
activeToolFilters.clear();
activeToolFilters.add(tool);
}
applyFilters();
var rows = document.querySelectorAll('.tool-perf-row');
rows.forEach(function(r) {
r.classList.toggle('active', r.dataset.tool === activeToolPerfFilter);
});
});
row.dataset.tool = tool;
row.appendChild(h('div', { className: 'tool-perf-dot', style: 'background:' + color }));
var toolDispName = fmtMcpName(tool);
row.appendChild(h('span', { className: 'tool-perf-name', title: tool }, toolDispName));
row.appendChild(h('span', { className: 'tool-perf-calls' }, String(stats.total)));
var barWrap = h('div', { className: 'tool-perf-bar-wrap' });
var okPct = stats.total > 0 ? (successes / stats.total * 100) : 100;
barWrap.appendChild(h('div', { className: 'tool-perf-bar-ok', style: 'width:' + okPct + '%' }));
if (stats.errors > 0) {
barWrap.appendChild(h('div', { className: 'tool-perf-bar-err', style: 'width:' + (100 - okPct) + '%' }));
}
row.appendChild(barWrap);
row.appendChild(h('span', { className: rateCls }, rateStr));
var errCls = 'tool-perf-errs' + (stats.errors > 0 ? ' has-errors' : '');
row.appendChild(h('span', { className: errCls }, stats.errors > 0 ? stats.errors + ' err' : ''));
if (stats.retries > 0) {
var retryEl = h('div', { className: 'tool-perf-retries' },
'\u21bb ' + stats.retries + ' retr' + (stats.retries === 1 ? 'y' : 'ies') +
' across ' + stats.chapters + ' chapter' + (stats.chapters === 1 ? '' : 's'));
row.appendChild(retryEl);
}
return row;
}
function rebuildToolPerfTable(sortedEntries) {
var t = document.getElementById('tool-perf-table');
if (!t) return;
t.innerHTML = '';
for (var idx = 0; idx < sortedEntries.length; idx++) {
t.appendChild(buildToolPerfRow(sortedEntries[idx][0], sortedEntries[idx][1]));
}
}
function renderWaveform() {
const turns = [];
for (const e of events) {
if (e.type !== 'assistant' || !e.tokens) continue;
const ts = new Date(e.timestamp).getTime();
const tools = (e.toolCalls || []).map(tc => tc.name);
const primaryTool = tools[0] || '_text';
const hasError = events.some(r => r.type === 'tool_result' && r.isError && e.toolCalls?.some(tc => tc.id === r.toolUseId));
const cacheRatio = e.tokens.input > 0 ? e.tokens.cacheHit / e.tokens.input : 1;
turns.push({
ms: ts,
output: e.tokens.output,
input: e.tokens.input,
cacheRatio,
tool: primaryTool,
tools,
hasError,
timestamp: e.timestamp,
text: e.text,
});
}
if (!turns.length) return h('div');
const userMsgs = [];
for (const e of events) {
if (e.type !== 'user' || !e.timestamp) continue;
const ts = new Date(e.timestamp).getTime();
let idx = 0;
for (let i = 0; i < turns.length; i++) {
if (turns[i].ms >= ts) { idx = i; break; }
idx = i + 1;
}
userMsgs.push({ idx, timestamp: e.timestamp });
}
const turnToChapter = new Array(turns.length).fill(0);
for (let c = 0; c < userMsgs.length; c++) {
const start = userMsgs[c].idx;
const end = c + 1 < userMsgs.length ? userMsgs[c + 1].idx : turns.length;
for (let t = start; t < end; t++) turnToChapter[t] = c;
}
const gaps = [];
for (let i = 1; i < turns.length; i++) {
const delta = turns[i].ms - turns[i - 1].ms;
if (delta > 30000) gaps.push({ idx: i, delta });
}
let maxOut = 1;
let maxIn = 1;
for (let i = 0; i < turns.length; i++) {
const o = turns[i].output;
const inp = turns[i].input;
if (o > maxOut) maxOut = o;
if (inp > maxIn) maxIn = inp;
}
const wrap = h('div', { className: 'waveform-wrap' });
wrap.appendChild(h('div', { className: 'waveform-label' }, '▲ output · ▼ input · brightness = uncached — click to jump to chapter'));
const canvas = document.createElement('canvas');
canvas.className = 'waveform-canvas';
const HEIGHT = 200;
wrap.appendChild(canvas);
const tooltip = h('div', { className: 'waveform-tooltip' });
wrap.appendChild(tooltip);
const cursor = h('div', { className: 'waveform-cursor' });
wrap.appendChild(cursor);
const regionHL = document.createElement('div');
regionHL.style.cssText = 'position:absolute;background:rgba(139,124,246,0.06);pointer-events:none;display:none;border-radius:2px;z-index:5;';
wrap.appendChild(regionHL);
let canvasW = 0;
let hoveredChapter = -1;
setTimeout(() => { canvasW = drawWaveform(canvas, turns, gaps, userMsgs, maxOut, maxIn, HEIGHT); }, 0);
window.addEventListener('resize', () => { canvasW = drawWaveform(canvas, turns, gaps, userMsgs, maxOut, maxIn, HEIGHT); });
canvas.addEventListener('mousemove', (ev) => {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left;
const idx = Math.min(turns.length - 1, Math.max(0, ((mx / rect.width) * turns.length) | 0));
const turn = turns[idx];
const oL = canvas.offsetLeft;
const oT = canvas.offsetTop;
const cW = rect.width / turns.length;
cursor.style.display = 'block';
cursor.style.left = (oL + mx) + 'px';
cursor.style.top = oT + 'px';
cursor.style.height = canvas.style.height;
const chIdx = turnToChapter[idx];
const rStart = userMsgs[chIdx] ? userMsgs[chIdx].idx : 0;
const rEnd = chIdx + 1 < userMsgs.length ? userMsgs[chIdx + 1].idx : turns.length;
regionHL.style.display = 'block';
regionHL.style.left = (oL + rStart * cW) + 'px';
regionHL.style.top = oT + 'px';
regionHL.style.width = ((rEnd - rStart) * cW) + 'px';
regionHL.style.height = canvas.style.height;
if (chIdx !== hoveredChapter) {
if (hoveredChapter >= 0) {
const prev = document.getElementById('chapter-' + hoveredChapter);
if (prev) prev.classList.remove('wf-hover');
}
hoveredChapter = chIdx;
const chEl = document.getElementById('chapter-' + chIdx);
if (chEl) chEl.classList.add('wf-hover');
}
if (turn) {
const cacheP = ((1 - turn.cacheRatio) * 100) | 0;
tooltip.style.display = 'block';
const tx = Math.min(mx + 12, (canvasW || rect.width) - 220);
tooltip.style.left = (oL + tx) + 'px';
tooltip.style.top = (oT + 14) + 'px';
tooltip.innerHTML =
'<span style="color:var(--fg3)">chapter ' + (chIdx + 1) + '</span> · ' +
'<b>' + formatTime(turn.timestamp) + '</b> ' +
(turn.tools.length ? turn.tools.join(', ') : 'text') +
'<br>out: ' + fmtTokens(turn.output) +
' · in: ' + fmtTokens(turn.input) +
' · cold: ' + cacheP + '%' +
(turn.hasError ? ' · <span style="color:#f07070">ERROR</span>' : '');
} else {
tooltip.style.display = 'none';
}
});
canvas.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
cursor.style.display = 'none';
regionHL.style.display = 'none';
if (hoveredChapter >= 0) {
const prev = document.getElementById('chapter-' + hoveredChapter);
if (prev) prev.classList.remove('wf-hover');
hoveredChapter = -1;
}
});
canvas.addEventListener('click', (ev) => {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left;
const idx = Math.min(turns.length - 1, Math.max(0, ((mx / rect.width) * turns.length) | 0));
jumpToChapter(turnToChapter[idx]);
});
const usedTools = [...new Set(turns.map(t => t.tool))];
const legend = h('div', { className: 'waveform-legend' });
for (const tool of usedTools) {
const name = tool.startsWith('_') ? (tool === '_text' ? 'text' : tool.slice(1)) : fmtMcpName(tool);
const color = getToolColor(tool);
legend.appendChild(h('div', { className: 'legend-item' },
h('div', { className: 'legend-dot', style: 'background:' + color }),
name
));
}
wrap.appendChild(legend);
return wrap;
}
function renderCostChart() {
const chapters = getChapters();
if (chapters.length < 2) return h('div');
const chapterCosts = [];
let cumulative = 0;
let hasCost = false;
for (const ch of chapters) {
const cost = estimateChapterTokenCost(session.model, ch.tokens);
cumulative += cost;
chapterCosts.push({ cost, cumulative });
if (cost > 0) hasCost = true;
}
if (!hasCost || cumulative < 0.001) return h('div');
const totalCostStr = fmtCost(cumulative, { prefix: '$' });
const maxCumulative = cumulative;
let maxSingle = 0;
for (let i = 0; i < chapterCosts.length; i++) {
const c = chapterCosts[i].cost;
if (c > maxSingle) maxSingle = c;
}
const wrap = h('div', { className: 'cost-chart' });
wrap.appendChild(h('div', { className: 'cost-chart-header' },
'cost progression',
h('span', { className: 'cost-chart-total' }, 'total: ~' + totalCostStr)
));
const canvas = document.createElement('canvas');
canvas.className = 'cost-chart-canvas';
const HEIGHT = 100;
wrap.appendChild(canvas);
const tooltip = h('div', { className: 'cost-chart-tooltip' });
wrap.appendChild(tooltip);
let chartW = 0;
setTimeout(function() { chartW = drawChart(canvas, chapterCosts, maxCumulative, maxSingle, HEIGHT); }, 0);
window.addEventListener('resize', function() { chartW = drawChart(canvas, chapterCosts, maxCumulative, maxSingle, HEIGHT); });
canvas.addEventListener('mousemove', function(ev) {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left;
const numCh = chapterCosts.length;
const idx = Math.min(numCh - 1, Math.max(0, ((mx / rect.width) * numCh) | 0));
const c = chapterCosts[idx];
tooltip.style.display = 'block';
const costStr = fmtCost(c.cost, {
prefix: '$',
min: 0.001,
fine: true,
tinyMin: 0.001,
tinyLabel: '<$0.001',
zeroLabel: '<$0.001',
});
const cumStr = fmtCost(c.cumulative, { prefix: '$' });
tooltip.innerHTML =
'<b>chapter ' + (idx + 1) + '</b><br>' +
'cost: <span style="color:var(--orange)">' + costStr + '</span> · ' +
'cumulative: <span style="color:var(--orange)">' + cumStr + '</span>';
const oL = canvas.offsetLeft;
const oT = canvas.offsetTop;
const tx = Math.min(mx + 12, (chartW || rect.width) - 180);
tooltip.style.left = (oL + tx) + 'px';
tooltip.style.top = (oT + 8) + 'px';
});
canvas.addEventListener('mouseleave', function() {
tooltip.style.display = 'none';
});
canvas.addEventListener('click', function(ev) {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left;
const numCh = chapterCosts.length;
const idx = Math.min(numCh - 1, Math.max(0, ((mx / rect.width) * numCh) | 0));
jumpToChapter(idx);
});
const axis = h('div', { className: 'cost-chart-axis' });
axis.appendChild(h('span', {}, 'ch 1'));
axis.appendChild(h('span', {}, 'ch ' + chapterCosts.length));
wrap.appendChild(axis);
return wrap;
}
function renderToolFlow() {
const chapters = getChapters();
const orderedSequence = [];
let chIdx = -1;
for (const e of events) {
if (e.type === 'user' && e.text) {
chIdx++;
}
if (e.type === 'assistant' && e.toolCalls && chIdx >= 0) {
for (const tc of e.toolCalls) {
orderedSequence.push({ tool: tc.name, chapterIdx: chIdx });
}
}
}
if (orderedSequence.length < 3) return h('div');
const orderedBoundaries = new Set();
let prevCh = -1;
for (let i = 0; i < orderedSequence.length; i++) {
if (orderedSequence[i].chapterIdx !== prevCh) {
orderedBoundaries.add(i);
prevCh = orderedSequence[i].chapterIdx;
}
}
const toolTotals = {};
for (const item of orderedSequence) {
toolTotals[item.tool] = (toolTotals[item.tool] || 0) + 1;
}
const transitions = {};
for (let i = 0; i < orderedSequence.length - 1; i++) {
const from = orderedSequence[i].tool;
const to = orderedSequence[i + 1].tool;
if (orderedSequence[i].chapterIdx !== orderedSequence[i + 1].chapterIdx) continue;
const key = from + '>' + to;
transitions[key] = (transitions[key] || 0) + 1;
}
const sortedToolNames = [];
for (const tool in toolTotals) {
if (Object.prototype.hasOwnProperty.call(toolTotals, tool)) sortedToolNames.push(tool);
}
sortedToolNames.sort((a, b) => toolTotals[b] - toolTotals[a]);
const numChapters = chapters.length;
const wrap = h('div', { className: 'tool-flow' });
wrap.appendChild(h('div', { className: 'tool-flow-header' }, 'tool usage flow'));
const seqWrap = h('div', { className: 'tool-flow-sequence' });
const maxCells = 500;
const step = orderedSequence.length > maxCells ? Math.ceil(orderedSequence.length / maxCells) : 1;
for (let i = 0; i < orderedSequence.length; i += step) {
if (orderedBoundaries.has(i) && i > 0) {
seqWrap.appendChild(h('div', { className: 'tool-flow-divider' }));
}
const item = orderedSequence[i];
const color = getToolColor(item.tool);
const displayName = fmtMcpName(item.tool);
const cell = h('div', {
className: 'tool-flow-cell',
title: displayName + ' (ch ' + (item.chapterIdx + 1) + ')'
});
cell.style.background = color;
seqWrap.appendChild(cell);
}
wrap.appendChild(seqWrap);
const summaryGrid = h('div', { className: 'tool-flow-summary' });
for (let ti = 0; ti < sortedToolNames.length && ti < 8; ti++) {
const tool = sortedToolNames[ti];
const count = toolTotals[tool];
const color = getToolColor(tool);
const avg = (count / numChapters).toFixed(1);
const toolLabel = fmtMcpName(tool);
const stat = h('div', { className: 'tool-flow-stat' },
h('div', { className: 'tool-flow-stat-dot', style: 'background:' + color }),
h('span', { className: 'tool-flow-stat-name' }, toolLabel),
h('span', { className: 'tool-flow-stat-count' }, String(count)),
h('span', { className: 'tool-flow-stat-avg' }, avg + '/ch')
);
summaryGrid.appendChild(stat);
}
wrap.appendChild(summaryGrid);
const sortedTransitionKeys = [];
for (const key in transitions) {
if (Object.prototype.hasOwnProperty.call(transitions, key)) sortedTransitionKeys.push(key);
}
sortedTransitionKeys.sort((a, b) => transitions[b] - transitions[a]);
if (sortedTransitionKeys.length > 0) {
const transWrap = h('div', { className: 'tool-flow-transitions' });
transWrap.appendChild(h('div', { className: 'tool-flow-transitions-label' }, 'common transitions'));
const list = h('div', { className: 'tool-flow-transition-list' });
for (let tri = 0; tri < sortedTransitionKeys.length && tri < 10; tri++) {
const key = sortedTransitionKeys[tri];
const count = transitions[key];
const from = key.slice(0, key.indexOf('>'));
const to = key.slice(key.indexOf('>') + 1);
var fromLabel = fmtMcpName(from);
var toLabel = fmtMcpName(to);
list.appendChild(h('div', { className: 'tool-flow-transition' },
h('span', { style: 'color:' + getToolColor(from) }, fromLabel),
h('span', { className: 'tool-flow-transition-arrow' }, '→'),
h('span', { style: 'color:' + getToolColor(to) }, toLabel),
h('span', { className: 'tool-flow-transition-count' }, '×' + count)
));
}
transWrap.appendChild(list);
wrap.appendChild(transWrap);
}
return wrap;
}
let toolPerfExpanded = false;
let toolPerfSort = 'errors'; // 'errors' (most error-prone first) or 'usage' (most used first)
let activeToolPerfFilter = null; // tool name being filtered on via click
function renderToolPerformance() {
const chapters = getChapters();
const toolStats = {};
for (const ch of chapters) {
const toolKeys = chapterToolKeys(ch);
for (let tki = 0; tki < toolKeys.length; tki++) {
const tool = toolKeys[tki];
const count = ch.toolCounts[tool];
if (!toolStats[tool]) toolStats[tool] = { total: 0, errors: 0, retries: 0, chapters: 0 };
toolStats[tool].total += count;
toolStats[tool].chapters++;
}
for (const tool in ch.errorTools) {
if (!Object.prototype.hasOwnProperty.call(ch.errorTools, tool)) continue;
const count = ch.errorTools[tool];
if (!toolStats[tool]) toolStats[tool] = { total: 0, errors: 0, retries: 0, chapters: 0 };
toolStats[tool].errors += count;
}
for (const rg of (ch.retryGroups || [])) {
if (!toolStats[rg.tool]) toolStats[rg.tool] = { total: 0, errors: 0, retries: 0, chapters: 0 };
toolStats[rg.tool].retries += rg.count - 1;
}
}
const toolStatNames = [];
for (const name in toolStats) {
if (Object.prototype.hasOwnProperty.call(toolStats, name)) toolStatNames.push(name);
}
if (toolStatNames.length < 2) return h('div');
const entries = toolStatNames.map(function(name) { return [name, toolStats[name]]; });
var sorted = sortToolPerfEntries(entries, toolPerfSort);
var wrap = h('div', { className: 'tool-perf' + (toolPerfExpanded ? ' expanded' : '') });
var totalCalls = 0;
var totalErrors = 0;
for (let tsi = 0; tsi < toolStatNames.length; tsi++) {
const st = toolStats[toolStatNames[tsi]];
totalCalls += st.total;
totalErrors += st.errors;
}
var overallRate = totalCalls > 0 ? ((totalCalls - totalErrors) / totalCalls * 100).toFixed(0) : '100';
var header = h('div', { className: 'tool-perf-header' });
header.appendChild(h('span', { className: 'tool-perf-title' },
'tool performance — ' + entries.length + ' tools, ' + overallRate + '% success'));
header.appendChild(h('span', { className: 'tool-perf-toggle' },
toolPerfExpanded ? '\u25be' : '\u25b8'));
header.addEventListener('click', function(ev) {
ev.stopPropagation();
toolPerfExpanded = !toolPerfExpanded;
wrap.classList.toggle('expanded');
header.querySelector('.tool-perf-toggle').textContent = toolPerfExpanded ? '\u25be' : '\u25b8';
});
wrap.appendChild(header);
var body = h('div', { className: 'tool-perf-body' });
var sortBar = h('div', { className: 'tool-perf-sort' });
['errors', 'usage'].forEach(function(mode) {
var btn = h('button', {
className: 'tool-perf-sort-btn' + (toolPerfSort === mode ? ' active' : ''),
onClick: function(ev) {
ev.stopPropagation();
toolPerfSort = mode;
var newSorted = sortToolPerfEntries(entries, mode);
rebuildToolPerfTable(newSorted);
sortBar.querySelectorAll('.tool-perf-sort-btn').forEach(function(b) {
b.classList.toggle('active', b.textContent === mode);
});
}
}, mode);
sortBar.appendChild(btn);
});
body.appendChild(sortBar);
var table = h('div', { className: 'tool-perf-table', id: 'tool-perf-table' });
for (var idx = 0; idx < sorted.length; idx++) {
table.appendChild(buildToolPerfRow(sorted[idx][0], sorted[idx][1]));
}
body.appendChild(table);
wrap.appendChild(body);
return wrap;
}
let fileHotspotExpanded = false;
let activeFileFilter = null; // path string or null
function renderFileHotspot() {
const chapters = getChapters();
const fileMap = {};
for (let i = 0; i < chapters.length; i++) {
const ch = chapters[i];
const fileKeys = chapterFileKeys(ch);
for (let fki = 0; fki < fileKeys.length; fki++) {
const path = fileKeys[fki];
const info = ch.files[path];
if (!fileMap[path]) fileMap[path] = { total: 0, ops: {}, chapters: new Set() };
const fm = fileMap[path];
fm.chapters.add(i);
for (const op of info.ops) {
fm.ops[op] = (fm.ops[op] || 0) + 1;
fm.total++;
}
}
}
const filePaths = [];
for (const path in fileMap) {
if (Object.prototype.hasOwnProperty.call(fileMap, path)) filePaths.push(path);
}
if (filePaths.length < 2) return h('div');
filePaths.sort((a, b) => fileMap[b].total - fileMap[a].total);
const maxTouch = fileMap[filePaths[0]].total;
const topFiles = filePaths.slice(0, 30).map(function(path) { return [path, fileMap[path]]; });
const entries = filePaths.map(function(path) { return [path, fileMap[path]]; });
const wrap = h('div', { className: 'file-hotspot' + (fileHotspotExpanded ? ' expanded' : '') });
const header = h('div', { className: 'file-hotspot-header', onClick: (ev) => {
ev.stopPropagation();
fileHotspotExpanded = !fileHotspotExpanded;
wrap.classList.toggle('expanded');
}});
header.appendChild(h('span', { className: 'file-hotspot-toggle' }, fileHotspotExpanded ? '▾' : '▸'));
header.appendChild(h('span', { className: 'file-hotspot-title' }, 'file hotspots'));
header.appendChild(h('span', { className: 'file-hotspot-count' }, entries.length + ' files · ' + entries.reduce((s, e) => s + e[1].total, 0) + ' touches'));
wrap.appendChild(header);
const body = h('div', { className: 'file-hotspot-body' });
const bar = h('div', { className: 'file-hotspot-bar' });
const totalTouches = entries.reduce((s, e) => s + e[1].total, 0);
for (const [path, info] of topFiles.slice(0, 20)) {
const pct = (info.total / totalTouches) * 100;
if (pct < 0.5) continue;
let maxOpName = '';
let maxOpCount = 0;
for (const op in info.ops) {
if (!Object.prototype.hasOwnProperty.call(info.ops, op)) continue;
if (info.ops[op] > maxOpCount) { maxOpCount = info.ops[op]; maxOpName = op; }
}
const opColors = { Read: '#6ba4e8', Edit: '#e0c45e', Write: '#d89660' };
const color = opColors[maxOpName] || '#8b7cf6';
const seg = h('div', { className: 'file-hotspot-bar-seg', title: path + ' (' + info.total + ')' });
seg.style.width = pct + '%';
seg.style.background = color;
seg.style.opacity = String(0.5 + 0.5 * (info.total / maxTouch));
bar.appendChild(seg);
}
body.appendChild(bar);
const list = h('div', { className: 'file-hotspot-list' });
for (const [path, info] of topFiles) {
const isActive = activeFileFilter === path;
const item = h('div', {
className: 'file-hotspot-item' + (isActive ? ' active' : ''),
title: path + ' — ' + info.total + ' touches across ' + info.chapters.size + ' chapters',
onClick: (ev) => {
ev.stopPropagation();
if (activeFileFilter === path) {
activeFileFilter = null;
} else {
activeFileFilter = path;
}
applyFileFilter();
}
});
const heatRatio = info.total / maxTouch;
const hue = 220 - (heatRatio * 180);
const sat = 60 + heatRatio * 30;
const lum = 45 + heatRatio * 15;
const dotSize = 6 + Math.round(heatRatio * 8);
const dot = h('div', { className: 'file-hotspot-dot' });
dot.style.background = 'hsl(' + hue + ',' + sat + '%,' + lum + '%)';
dot.style.width = dotSize + 'px';
dot.style.height = dotSize + 'px';
dot.style.boxShadow = heatRatio > 0.5 ? '0 0 ' + Math.round(heatRatio * 6) + 'px hsl(' + hue + ',' + sat + '%,' + lum + '%)' : 'none';
item.appendChild(dot);
item.appendChild(h('span', { className: 'file-hotspot-path' }, path));
const opsWrap = h('div', { className: 'file-hotspot-ops' });
const opNames = [];
for (const op in info.ops) {
if (Object.prototype.hasOwnProperty.call(info.ops, op)) opNames.push(op);
}
opNames.sort((a, b) => info.ops[b] - info.ops[a]);
for (let opi = 0; opi < opNames.length; opi++) {
const op = opNames[opi];
const count = info.ops[op];
const cls = op.toLowerCase();
opsWrap.appendChild(h('span', { className: 'file-hotspot-op ' + cls }, op[0] + count));
}
item.appendChild(opsWrap);
const chList = [...info.chapters].sort((a, b) => a - b);
const chStr = chList.length <= 4
? chList.map(c => c + 1).join(',')
: chList.slice(0, 3).map(c => c + 1).join(',') + '…+' + (chList.length - 3);
item.appendChild(h('span', { className: 'file-hotspot-chapters' }, 'ch ' + chStr));
item.appendChild(h('span', { className: 'file-hotspot-touch' }, String(info.total)));
list.appendChild(item);
}
body.appendChild(list);
if (entries.length > 30) {
body.appendChild(h('div', { style: 'font-size:11px;color:var(--fg3);margin-top:6px;padding-left:10px;' }, '… ' + (entries.length - 30) + ' more files'));
}
wrap.appendChild(body);
return wrap;
}
function renderGitTimeline() {
const chapters = getChapters();
const allGitOps = [];
for (let i = 0; i < chapters.length; i++) {
for (const op of chapters[i].gitOps) {
allGitOps.push({ ...op, chapterIdx: i });
}
}
if (allGitOps.length === 0) return h('div');
const wrap = h('div', { className: 'git-timeline' });
const header = h('div', { className: 'git-timeline-header' });
header.appendChild(h('span', { className: 'git-timeline-title' }, 'git activity'));
var commits = 0, pushes = 0, branches = 0;
for (var gi = 0; gi < allGitOps.length; gi++) {
var gt = allGitOps[gi].type;
if (gt === 'commit') commits++;
else if (gt === 'push') pushes++;
else if (gt === 'branch-create' || gt === 'branch-switch') branches++;
}
var countParts = [];
if (commits > 0) countParts.push(commits + ' commit' + (commits > 1 ? 's' : ''));
if (pushes > 0) countParts.push(pushes + ' push' + (pushes > 1 ? 'es' : ''));
if (branches > 0) countParts.push(branches + ' branch op' + (branches > 1 ? 's' : ''));
if (countParts.length === 0) countParts.push(allGitOps.length + ' operation' + (allGitOps.length > 1 ? 's' : ''));
header.appendChild(h('span', { className: 'git-timeline-count' }, countParts.join(' · ')));
wrap.appendChild(header);
const opsDiv = h('div', { className: 'git-timeline-ops' });
for (const op of allGitOps.slice(0, 20)) {
var iconCls = 'git-op-icon ';
var icon = '';
if (op.type === 'commit') { iconCls += 'commit'; icon = '●'; }
else if (op.type === 'push') { iconCls += 'push'; icon = '↑'; }
else if (op.type === 'branch-create') { iconCls += 'branch'; icon = '⑃'; }
else if (op.type === 'branch-switch') { iconCls += 'branch'; icon = '⇢'; }
else if (op.type === 'merge') { iconCls += 'merge'; icon = '⑂'; }
else if (op.type === 'rebase') { iconCls += 'merge'; icon = '↻'; }
else if (op.type === 'tag') { iconCls += 'push'; icon = '◆'; }
else { iconCls += 'other'; icon = '·'; }
var detail = h('span', { className: 'git-op-detail' });
if (op.type === 'commit') {
if (op.hash) detail.appendChild(h('span', { className: 'git-op-hash' }, op.hash.slice(0, 7)));
detail.appendChild(h('span', { className: 'git-op-msg' }, op.message || '(no message)'));
} else if (op.type === 'push') {
var pushText = 'push';
if (op.remote) pushText += ' ' + op.remote;
if (op.branch) pushText += ' ' + op.branch;
if (op.tags) pushText += ' --tags';
detail.appendChild(h('span', { className: 'git-op-msg' }, pushText));
} else if (op.type === 'branch-create') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'create '));
detail.appendChild(h('span', { className: 'git-op-branch-name' }, op.branch || '?'));
} else if (op.type === 'branch-switch') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'switch to '));
detail.appendChild(h('span', { className: 'git-op-branch-name' }, op.branch || '?'));
} else if (op.type === 'merge') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'merge '));
detail.appendChild(h('span', { className: 'git-op-branch-name' }, op.branch || '?'));
} else if (op.type === 'rebase') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'rebase onto '));
detail.appendChild(h('span', { className: 'git-op-branch-name' }, op.branch || '?'));
} else if (op.type === 'tag') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'tag '));
detail.appendChild(h('span', { className: 'git-op-branch-name' }, op.tag || '?'));
} else if (op.type === 'stash') {
detail.appendChild(h('span', { className: 'git-op-msg' }, 'stash'));
} else {
detail.appendChild(h('span', { className: 'git-op-msg' }, op.type));
}
var chapterLink = h('span', {
className: 'git-op-chapter',
onClick: (function(idx) { return function(ev) {
ev.stopPropagation();
jumpToChapter(idx);
}; })(op.chapterIdx)
}, 'ch ' + (op.chapterIdx + 1));
opsDiv.appendChild(h('div', { className: 'git-op' },
h('span', { className: iconCls }, icon),
detail,
chapterLink
));
}
if (allGitOps.length > 20) {
opsDiv.appendChild(h('div', { className: 'git-op' },
h('span', { className: 'git-op-icon other' }, '…'),
h('span', { className: 'git-op-detail' },
h('span', { className: 'git-op-msg' }, (allGitOps.length - 20) + ' more operations')
)
));
}
wrap.appendChild(opsDiv);
return wrap;
}
function applyFileFilter() {
const chapters = getChapters();
const items = document.querySelectorAll('.file-hotspot-item');
items.forEach(function(item) {
const path = item.querySelector('.file-hotspot-path');
if (!path) return;
if (activeFileFilter && path.textContent === activeFileFilter) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
if (activeFileFilter) {
let visible = 0;
for (let i = 0; i < chapters.length; i++) {
const el = document.getElementById('chapter-' + i);
if (!el) continue;
const hasFile = activeFileFilter in chapters[i].files;
const matchesOther = chapterMatchesFilter(chapters[i]);
if (hasFile && matchesOther) {
el.classList.remove('filter-hidden');
visible++;
} else {
el.classList.add('filter-hidden');
}
}
const countEl = document.getElementById('filter-count');
if (countEl) countEl.textContent = visible + '/' + chapters.length;
} else {
applyFilters();
}
}
var minimapEl = null;
var minimapTooltipEl = null;
var minimapViewportEl = null;
var minimapBlocks = [];
var minimapScrollTicking = false;
var MINIMAP_THRESHOLD = 10;
function renderMiniMap() {
if (minimapEl && minimapEl.parentNode) minimapEl.parentNode.removeChild(minimapEl);
if (minimapTooltipEl && minimapTooltipEl.parentNode) minimapTooltipEl.parentNode.removeChild(minimapTooltipEl);
minimapBlocks = [];
var chapters = getChapters();
if (chapters.length < MINIMAP_THRESHOLD) return;
if (!minimapTooltipEl) {
minimapTooltipEl = document.createElement('div');
minimapTooltipEl.className = 'minimap-tooltip';
document.body.appendChild(minimapTooltipEl);
}
minimapEl = document.createElement('div');
minimapEl.className = 'minimap';
var maxTokens = 0;
for (var i = 0; i < chapters.length; i++) {
var tok = chapters[i].tokens.input + chapters[i].tokens.output;
if (tok > maxTokens) maxTokens = tok;
}
var useTurns = maxTokens === 0;
var maxTurns = 0;
if (useTurns) {
for (var i = 0; i < chapters.length; i++) {
if (chapters[i].turns > maxTurns) maxTurns = chapters[i].turns;
}
}
// Budget: approx 80vh minus padding/gaps. Each block: min 2px, max 16px.
var viewH = window.innerHeight * 0.8;
var gapSpace = (chapters.length - 1) * 1 + 8;
var availH = viewH - gapSpace;
var minH = 2;
var maxH = Math.min(16, Math.max(minH, Math.floor(availH / chapters.length)));
minimapViewportEl = document.createElement('div');
minimapViewportEl.className = 'minimap-viewport';
minimapEl.appendChild(minimapViewportEl);
for (var i = 0; i < chapters.length; i++) {
var ch = chapters[i];
var outcome = ch.outcome || 'clean';
var val = useTurns
? (maxTurns > 0 ? ch.turns / maxTurns : 1)
: ((ch.tokens.input + ch.tokens.output) / maxTokens);
var height = Math.round(minH + val * (maxH - minH));
var block = document.createElement('div');
block.className = 'minimap-block mm-' + outcome;
block.style.height = height + 'px';
block.setAttribute('data-idx', String(i));
if (ch.errors > 0) {
var errDot = document.createElement('span');
errDot.className = 'minimap-marker mm-error';
block.appendChild(errDot);
}
if (countGitOpsOfType(ch.gitOps, 'commit') > 0) {
var commitDot = document.createElement('span');
commitDot.className = 'minimap-marker mm-commit';
block.appendChild(commitDot);
}
(function(idx) {
block.addEventListener('click', function() {
jumpToChapter(idx);
});
})(i);
(function(idx, blockEl) {
blockEl.addEventListener('mouseenter', function(ev) {
var c = chapters[idx];
if (!c) return;
var text = 'Ch ' + (idx + 1);
var promptSnip = c.prompt.slice(0, 50) + (c.prompt.length > 50 ? '...' : '');
text = text + ': ' + promptSnip;
minimapTooltipEl.textContent = text;
var r = blockEl.getBoundingClientRect();
var mmRight = window.innerWidth - r.left + 8;
minimapTooltipEl.style.top = (r.top + r.height / 2 - 12) + 'px';
minimapTooltipEl.style.left = 'auto';
minimapTooltipEl.style.right = mmRight + 'px';
minimapTooltipEl.classList.add('visible');
});
blockEl.addEventListener('mouseleave', function() {
minimapTooltipEl.classList.remove('visible');
});
})(i, block);
minimapBlocks.push(block);
minimapEl.appendChild(block);
}
document.body.appendChild(minimapEl);
}
function updateMiniMapViewport() {
if (!minimapEl || minimapBlocks.length === 0) return;
var chapters = getChapters();
if (chapters.length < MINIMAP_THRESHOLD) return;
var viewTop = window.scrollY;
var viewBottom = viewTop + window.innerHeight;
var firstVisible = -1;
var lastVisible = -1;
for (var i = 0; i < chapters.length; i++) {
var el = document.getElementById('chapter-' + i);
if (!el || el.classList.contains('filter-hidden')) continue;
var rect = el.getBoundingClientRect();
var elTop = rect.top + window.scrollY;
var elBottom = elTop + rect.height;
if (elBottom > viewTop && elTop < viewBottom) {
if (firstVisible === -1) firstVisible = i;
lastVisible = i;
}
}
for (var i = 0; i < minimapBlocks.length; i++) {
if (i >= firstVisible && i <= lastVisible && firstVisible !== -1) {
minimapBlocks[i].classList.add('mm-visible');
} else {
minimapBlocks[i].classList.remove('mm-visible');
}
}
if (minimapViewportEl && firstVisible >= 0 && lastVisible >= 0) {
var firstBlock = minimapBlocks[firstVisible];
var lastBlock = minimapBlocks[lastVisible];
if (firstBlock && lastBlock) {
var containerRect = minimapEl.getBoundingClientRect();
var firstRect = firstBlock.getBoundingClientRect();
var lastRect = lastBlock.getBoundingClientRect();
var vpTop = firstRect.top - containerRect.top;
var vpHeight = (lastRect.top + lastRect.height) - firstRect.top;
minimapViewportEl.style.top = vpTop + 'px';
minimapViewportEl.style.height = vpHeight + 'px';
minimapViewportEl.style.display = 'block';
}
} else if (minimapViewportEl) {
minimapViewportEl.style.display = 'none';
}
}
window.addEventListener('scroll', function() {
if (!minimapScrollTicking) {
minimapScrollTicking = true;
requestAnimationFrame(function() {
updateMiniMapViewport();
minimapScrollTicking = false;
});
}
});
window.addEventListener('resize', function() {
if (!minimapScrollTicking) {
minimapScrollTicking = true;
requestAnimationFrame(function() {
updateMiniMapViewport();
minimapScrollTicking = false;
});
}
});
function render() {
app.innerHTML = '';
chaptersCache = null;
app.appendChild(renderHeader());
app.appendChild(renderSessionSummary());
app.appendChild(renderActivityTimeline());
app.appendChild(renderStats());
app.appendChild(renderGitTimeline());
app.appendChild(renderErrorSummary());
app.appendChild(renderWaveform());
app.appendChild(renderToolFlow());
app.appendChild(renderToolPerformance());
app.appendChild(renderCostChart());
app.appendChild(renderFileHotspot());
app.appendChild(renderFilterBar());
app.appendChild(renderChapters());
requestAnimationFrame(function() {
attachExpandToggles();
attachChapterTooltips();
renderMiniMap();
if (activeFileFilter) applyFileFilter();
else applyFilters();
requestAnimationFrame(function() { updateMiniMapViewport(); });
});
}
function attachExpandToggles() {
var standalone = app.querySelectorAll('.chapter-output, .chapter-agent-prompt, .chapter-thinking-block, .chapter-mcp-output');
standalone.forEach(function(block) {
if (block.scrollHeight <= block.clientHeight + 2) return;
if (block.nextElementSibling && block.nextElementSibling.classList.contains('expand-toggle')) return;
var toggle = document.createElement('span');
toggle.className = 'expand-toggle';
toggle.textContent = '\u25b8 show more';
toggle.addEventListener('click', function(ev) {
ev.stopPropagation();
if (block.classList.contains('expanded')) {
block.classList.remove('expanded');
toggle.textContent = '\u25b8 show more';
} else {
block.classList.add('expanded');
toggle.textContent = '\u25be show less';
}
});
block.parentNode.insertBefore(toggle, block.nextSibling);
});
var diffBlocks = app.querySelectorAll('.chapter-diff-block');
diffBlocks.forEach(function(diffBlock) {
var children = diffBlock.querySelectorAll('.chapter-diff-del, .chapter-diff-add');
var anyTruncated = false;
children.forEach(function(child) {
if (child.scrollHeight > child.clientHeight + 2) anyTruncated = true;
});
if (!anyTruncated) return;
if (diffBlock.nextElementSibling && diffBlock.nextElementSibling.classList.contains('expand-toggle')) return;
var toggle = document.createElement('span');
toggle.className = 'expand-toggle in-diff';
toggle.textContent = '\u25b8 show more';
toggle.addEventListener('click', function(ev) {
ev.stopPropagation();
var isExpanded = children[0] && children[0].classList.contains('expanded');
children.forEach(function(child) {
if (isExpanded) child.classList.remove('expanded');
else child.classList.add('expanded');
});
if (isExpanded) {
toggle.textContent = '\u25b8 show more';
} else {
toggle.textContent = '\u25be show less';
}
});
diffBlock.parentNode.insertBefore(toggle, diffBlock.nextSibling);
});
}
const TOOL_DOT_COLORS = {
Bash: '#e8a44c', Read: '#8b7cf6', Edit: '#4ade80', Write: '#60a5fa',
Agent: '#f472b6', Grep: '#94a3b8', Skill: '#c084fc', WebSearch: '#38bdf8',
WebFetch: '#2dd4bf', TaskCreate: '#fb923c'
};
var tooltipEl = null;
var tooltipTimer = null;
var tooltipVisible = false;
function getTooltipEl() {
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.className = 'chapter-tooltip';
document.body.appendChild(tooltipEl);
}
return tooltipEl;
}
function showChapterTooltip(chapterIdx, anchorEl) {
var chapters = getChapters();
var ch = chapters[chapterIdx];
if (!ch) return;
var tip = getTooltipEl();
var html = '';
var promptRaw = ch.prompt || '';
var promptNl = promptRaw.indexOf('\n');
var promptFirst = promptNl < 0 ? promptRaw : promptRaw.slice(0, promptNl);
var promptLine = promptFirst.slice(0, 80);
if (promptLine.length < promptFirst.length) promptLine += '...';
html += '<div class="chapter-tooltip-prompt">' + esc(promptLine) + '</div>';
var toolKeys = chapterToolKeys(ch);
var toolNames = toolKeys.slice().sort(function(a, b) { return ch.toolCounts[b] - ch.toolCounts[a]; });
if (toolNames.length > 0) {
html += '<div class="chapter-tooltip-tools">';
for (var i = 0; i < Math.min(toolNames.length, 8); i++) {
var tColor = TOOL_DOT_COLORS[toolNames[i]] || (typeof toolNames[i] === 'string' && toolNames[i].startsWith('mcp__') ? '#5dadec' : '#6b7280');
var tTitle = fmtMcpName(toolNames[i]);
html += '<span class="chapter-tooltip-tool" style="background:' + tColor + '" title="' + esc(tTitle) + ' ' + ch.toolCounts[toolNames[i]] + '"></span>';
}
if (toolNames.length > 8) html += '<span style="color:var(--fg3);font-size:10px">+' + (toolNames.length - 8) + '</span>';
html += '</div>';
}
if (ch.errors > 0) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">errors</span><span class="chapter-tooltip-value errors">' + ch.errors + '</span></div>';
}
var filePaths = chapterFileKeys(ch);
if (filePaths.length > 0) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">files</span><span class="chapter-tooltip-value">' + filePaths.length + ' file' + (filePaths.length !== 1 ? 's' : '') + '</span></div>';
html += '<div class="chapter-tooltip-files">';
for (var fi = 0; fi < Math.min(filePaths.length, 3); fi++) {
var short = shortToolPath(filePaths[fi]);
html += '<div class="chapter-tooltip-file">' + esc(short) + '</div>';
}
if (filePaths.length > 3) html += '<div class="chapter-tooltip-file" style="color:var(--fg3)">+' + (filePaths.length - 3) + ' more</div>';
html += '</div>';
}
if (ch.mcpOps.length > 0) {
var mcpServers = {};
for (var mti = 0; mti < ch.mcpOps.length; mti++) { mcpServers[ch.mcpOps[mti].server] = (mcpServers[ch.mcpOps[mti].server] || 0) + 1; }
var mcpSummary = '';
var mcpFirst = true;
for (var srv in mcpServers) {
if (!Object.prototype.hasOwnProperty.call(mcpServers, srv)) continue;
if (!mcpFirst) mcpSummary += ', ';
mcpFirst = false;
mcpSummary += srv.replace(/_/g, ' ') + ' \u00d7' + mcpServers[srv];
}
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">MCP</span><span class="chapter-tooltip-value" style="color:#5dadec">' + esc(mcpSummary) + '</span></div>';
}
var endMs = new Date(ch.endTimestamp).getTime();
var startMs = new Date(ch.timestamp).getTime();
var dur = endMs - startMs;
if (dur > 1000) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">duration</span><span class="chapter-tooltip-value">' + formatDuration(dur) + '</span></div>';
}
var totalTok = ch.tokens.input + ch.tokens.output;
if (totalTok > 0) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">tokens</span><span class="chapter-tooltip-value">' + fmtTokens(totalTok) + '</span></div>';
}
if (ch.deps && ch.deps.fixesFrom.length > 0) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">fixes</span><span class="chapter-tooltip-value" style="color:var(--green)">ch ' + ch.deps.fixesFrom.map(function(r){return r+1;}).join(', ') + '</span></div>';
} else if (ch.deps && ch.deps.continuesFrom.length > 0) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">continues</span><span class="chapter-tooltip-value" style="color:var(--accent)">ch ' + ch.deps.continuesFrom.map(function(r){return r+1;}).join(', ') + '</span></div>';
}
var outcome = ch.outcome || 'clean';
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">quality</span><span class="chapter-tooltip-value ' + outcome + '">' + outcome + '</span></div>';
if (ch.efficiency && ch.efficiency.isWasteful) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">efficiency</span><span class="chapter-tooltip-value" style="color:var(--orange)">' + ch.efficiency.score + '/100 \u26a0</span></div>';
} else if (ch.efficiency && ch.efficiency.score < 70) {
html += '<div class="chapter-tooltip-row"><span class="chapter-tooltip-label">efficiency</span><span class="chapter-tooltip-value" style="color:var(--fg2)">' + ch.efficiency.score + '/100</span></div>';
}
tip.innerHTML = html;
var rect = anchorEl.getBoundingClientRect();
var tipW = 300;
var tipH = tip.offsetHeight || 150;
var left = rect.left + 40;
var top = rect.bottom + 6;
if (left + tipW > window.innerWidth - 16) {
left = window.innerWidth - tipW - 16;
}
if (left < 8) left = 8;
if (top + tipH > window.innerHeight - 16) {
top = rect.top - tipH - 6;
}
tip.style.left = left + 'px';
tip.style.top = top + 'px';
tip.classList.add('visible');
tooltipVisible = true;
}
function hideChapterTooltip() {
if (tooltipTimer) { clearTimeout(tooltipTimer); tooltipTimer = null; }
if (tooltipEl) {
tooltipEl.classList.remove('visible');
}
tooltipVisible = false;
}
function attachChapterTooltips() {
var chapterEls = document.querySelectorAll('.chapter');
chapterEls.forEach(function(el) {
var idx = parseInt(el.id.replace('chapter-', ''), 10);
if (isNaN(idx)) return;
el.addEventListener('mouseenter', function() {
if (el.classList.contains('expanded')) return;
tooltipTimer = setTimeout(function() {
if (!el.classList.contains('expanded')) {
showChapterTooltip(idx, el);
}
}, 300);
});
el.addEventListener('mouseleave', function() {
hideChapterTooltip();
});
});
}
window.addEventListener('scroll', function() {
if (tooltipVisible) hideChapterTooltip();
}, { passive: true });
let focusedChapterIdx = -1;
function getVisibleChapterIndices() {
var chapters = getChapters();
var visible = [];
for (var i = 0; i < chapters.length; i++) {
var el = document.getElementById('chapter-' + i);
if (el && !el.classList.contains('filter-hidden')) visible.push(i);
}
return visible;
}
function setFocusedChapter(idx) {
if (focusedChapterIdx >= 0) {
var prev = document.getElementById('chapter-' + focusedChapterIdx);
if (prev) prev.classList.remove('kb-focused');
}
focusedChapterIdx = idx;
if (idx >= 0) {
var el = document.getElementById('chapter-' + idx);
if (el) {
el.classList.add('kb-focused');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
}
function clearFocus() {
if (focusedChapterIdx >= 0) {
var prev = document.getElementById('chapter-' + focusedChapterIdx);
if (prev) prev.classList.remove('kb-focused');
}
focusedChapterIdx = -1;
}
document.addEventListener('keydown', function(ev) {
var searchInput = document.querySelector('.filter-search');
var isInSearch = document.activeElement === searchInput;
if (ev.key === 'Escape') {
if (isInSearch) {
searchInput.blur();
if (searchQuery) {
searchQuery = '';
searchInput.value = '';
applyFilters();
}
}
clearFocus();
return;
}
if (isInSearch) return;
var visible = getVisibleChapterIndices();
if (!visible.length) return;
if (ev.key === 'j' || ev.key === 'k') {
ev.preventDefault();
if (focusedChapterIdx < 0) {
setFocusedChapter(ev.key === 'j' ? visible[0] : visible[visible.length - 1]);
} else {
var currentPos = visible.indexOf(focusedChapterIdx);
if (currentPos < 0) {
setFocusedChapter(visible[0]);
} else {
var nextPos = ev.key === 'j' ? currentPos + 1 : currentPos - 1;
if (nextPos >= 0 && nextPos < visible.length) {
setFocusedChapter(visible[nextPos]);
}
}
}
return;
}
if (ev.key === 'Enter' || ev.key === 'o') {
if (focusedChapterIdx >= 0) {
ev.preventDefault();
var key = 'ch' + focusedChapterIdx;
if (expandedSet.has(key)) {
expandedSet.delete(key);
clearChapterHash();
} else {
expandedSet.add(key);
setChapterHash(focusedChapterIdx);
}
render();
requestAnimationFrame(function() {
setFocusedChapter(focusedChapterIdx);
});
}
return;
}
if (ev.key === '/') {
ev.preventDefault();
if (searchInput) searchInput.focus();
return;
}
});
const SECRET_RULES = [
{
"id": "openai-api-key",
"description": "OpenAI API Key",
"regex": "sk-[A-Za-z0-9]{16,}",
"keywords": ["sk-"],
"entropy": 3.0
},
{
"id": "github-pat",
"description": "GitHub Personal Access Token",
"regex": "ghp_[A-Za-z0-9]{20,}",
"keywords": ["ghp_"],
"entropy": 3.0
},
{
"id": "github-fine-grained-pat",
"description": "GitHub Fine-Grained Personal Access Token",
"regex": "github_pat_[A-Za-z0-9_]{20,}",
"keywords": ["github_pat_"],
"entropy": 3.0
},
{
"id": "aws-access-key",
"description": "AWS Access Key ID",
"regex": "AKIA[0-9A-Z]{16}",
"keywords": ["akia"],
"entropy": 2.5
},
{
"id": "generic-api-key",
"description": "Generic API Key Assignment",
"regex": "(?i)(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token)[\\s'\"`:=]{0,6}[\\s'\"`:=]{0,3}([A-Za-z0-9_\\-]{16,})",
"keywords": ["api", "key", "secret", "token"],
"secretGroup": 1,
"entropy": 3.0
},
{
"id": "bearer-token",
"description": "Bearer Token",
"regex": "(?i)bearer\\s+([A-Za-z0-9._\\-]{20,})",
"keywords": ["bearer"],
"secretGroup": 1,
"entropy": 3.0
},
{
"id": "private-key-block",
"description": "Private Key Block",
"regex": "-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
"keywords": ["private", "key"],
"flags": "g"
},
{
"id": "huggingface-token",
"description": "Hugging Face Access Token",
"regex": "hf_[A-Za-z0-9]{20,}",
"keywords": ["hf_"],
"entropy": 3.0
},
{
"id": "slack-token",
"description": "Slack Token",
"regex": "xox[baprs]-[0-9A-Za-z\\-]{10,}",
"keywords": ["xox"],
"entropy": 2.5
},
{
"id": "stripe-secret-key",
"description": "Stripe Secret Key",
"regex": "sk_(?:live|test)_[0-9a-zA-Z]{16,}",
"keywords": ["sk_live", "sk_test"],
"entropy": 3.0
}
];
function shannonEntropy(str) {
if (!str || str.length === 0) return 0;
const freq = new Map();
for (const c of str) freq.set(c, (freq.get(c) || 0) + 1);
let entropy = 0;
const len = str.length;
for (const count of freq.values()) {
const p = count / len;
entropy -= p * Math.log2(p);
}
return entropy;
}
function redactMatchDisplay(match) {
if (!match) return "****";
if (match.length <= 8) return "****";
return match.slice(0, 4) + "…" + match.slice(-4);
}
function normalizeRuleRegex(pattern, flags = "g") {
let source = String(pattern);
let outFlags = flags.includes("g") ? flags : `${flags}g`;
const inlineFlagRe = /^\(\?([imsu]+)\)/;
let match;
while ((match = inlineFlagRe.exec(source))) {
for (const ch of match[1]) {
const jsFlag = PCRE_INLINE_FLAG_MAP[ch];
if (jsFlag && !outFlags.includes(jsFlag)) outFlags += jsFlag;
}
source = source.slice(match[0].length);
}
return { source, flags: outFlags };
}
function compileRules(rules) {
return (rules || []).map((rule) => {
let regex;
try {
const { source, flags } = normalizeRuleRegex(rule.regex, rule.flags || "g");
regex = new RegExp(source, flags);
} catch {
return null;
}
const allowlist = (rule.allowlist?.regexes || []).map((pat) => {
try {
return new RegExp(pat);
} catch {
return null;
}
}).filter(Boolean);
const keywords = (rule.keywords || []).map((k) => String(k).toLowerCase());
return { ...rule, regex, allowlist, keywords };
}).filter(Boolean);
}
function textHasKeywords(text, keywords) {
if (!keywords.length) return true;
const lower = text.toLowerCase();
for (const kw of keywords) {
if (lower.includes(kw)) return true;
}
return false;
}
function isAllowlisted(match, allowlist) {
for (const re of allowlist) {
if (re.test(match)) return true;
}
return false;
}
function scanText(text, compiledRules, ctx, findings, seen) {
if (!text || typeof text !== "string") return;
for (const rule of compiledRules) {
if (!textHasKeywords(text, rule.keywords)) continue;
rule.regex.lastIndex = 0;
let m;
while ((m = rule.regex.exec(text)) !== null) {
const full = m[0];
const secret = rule.secretGroup != null ? (m[rule.secretGroup] || full) : full;
if (!secret) continue;
if (rule.entropy != null && shannonEntropy(secret) < rule.entropy) continue;
if (isAllowlisted(secret, rule.allowlist)) continue;
const key = `${rule.id}:${secret}:${ctx.eventIndex}:${ctx.field}`;
if (seen.has(key)) continue;
seen.add(key);
findings.push({
ruleId: rule.id,
description: rule.description,
match: redactMatchDisplay(secret),
secret,
location: {
chapterIndex: ctx.chapterIndex,
eventIndex: ctx.eventIndex,
eventType: ctx.eventType,
toolName: ctx.toolName || null,
field: ctx.field || null,
},
});
}
}
}
function scanObjectStrings(obj, compiledRules, ctx, findings, seen, depth = 0) {
if (!obj || typeof obj !== "object" || depth > 8) return;
if (Array.isArray(obj)) {
for (const item of obj) scanObjectStrings(item, compiledRules, ctx, findings, seen, depth + 1);
return;
}
for (const val of Object.values(obj)) {
if (typeof val === "string") scanText(val, compiledRules, ctx, findings, seen);
else if (val && typeof val === "object") scanObjectStrings(val, compiledRules, ctx, findings, seen, depth + 1);
}
}
function scanMcpInfo(mcpInfo, compiledRules, ctx, findings, seen) {
if (!mcpInfo) return;
if (typeof mcpInfo.params === "string") {
scanText(mcpInfo.params, compiledRules, { ...ctx, field: "mcpInfo.params" }, findings, seen);
}
scanObjectStrings(mcpInfo, compiledRules, { ...ctx, field: "mcpInfo" }, findings, seen);
}
function scanDiffInfo(diffInfo, compiledRules, ctx, findings, seen) {
if (!diffInfo) return;
if (diffInfo.oldStr) scanText(diffInfo.oldStr, compiledRules, { ...ctx, field: "diffInfo.oldStr" }, findings, seen);
if (diffInfo.newStr) scanText(diffInfo.newStr, compiledRules, { ...ctx, field: "diffInfo.newStr" }, findings, seen);
if (diffInfo.content) scanText(diffInfo.content, compiledRules, { ...ctx, field: "diffInfo.content" }, findings, seen);
}
function scanToolCall(tc, compiledRules, ctx, findings, seen) {
if (!tc) return;
const toolCtx = { ...ctx, toolName: tc.name || null, field: "toolCalls[].input" };
if (tc.input) scanText(tc.input, compiledRules, toolCtx, findings, seen);
scanDiffInfo(tc.diffInfo, compiledRules, { ...ctx, toolName: tc.name || null }, findings, seen);
scanMcpInfo(tc.mcpInfo, compiledRules, { ...ctx, toolName: tc.name || null }, findings, seen);
if (tc.agentInfo) scanObjectStrings(tc.agentInfo, compiledRules, { ...ctx, field: "agentInfo" }, findings, seen);
if (tc.webInfo) scanObjectStrings(tc.webInfo, compiledRules, { ...ctx, field: "webInfo" }, findings, seen);
}
function scanAgentHistory(agentHistory, compiledRules, findings, seen) {
if (!agentHistory || !Array.isArray(agentHistory)) return;
for (let i = 0; i < agentHistory.length; i++) {
const entry = agentHistory[i];
const ctx = {
chapterIndex: 0,
eventIndex: i,
eventType: "agentHistory",
toolName: entry?.agentId || null,
field: "agentHistory.record",
};
if (entry?.record) scanObjectStrings(entry.record, compiledRules, ctx, findings, seen);
}
}
function scanSessionScalars(session, compiledRules, findings, seen) {
const scalarFields = ["cwd", "gitBranch", "title", "model", "project", "sessionId"];
for (const field of scalarFields) {
const val = session[field];
if (typeof val !== "string" || !val) continue;
const ctx = {
chapterIndex: 0,
eventIndex: -1,
eventType: "session",
toolName: null,
field,
};
scanText(val, compiledRules, ctx, findings, seen);
}
}
function scanSessionForSecrets(session, rules) {
const compiledRules = compileRules(rules);
const findings = [];
const seen = new Set();
const events = session.events || [];
let chapterIndex = 0;
scanSessionScalars(session, compiledRules, findings, seen);
for (let eventIndex = 0; eventIndex < events.length; eventIndex++) {
const e = events[eventIndex];
if (eventIndex > 0 && e.type === "user" && e.text) chapterIndex++;
const ctx = {
chapterIndex,
eventIndex,
eventType: e.type,
toolName: null,
field: "text",
};
if (e.text) scanText(e.text, compiledRules, ctx, findings, seen);
if (Array.isArray(e.thinking)) {
for (const block of e.thinking) {
scanText(block, compiledRules, { ...ctx, field: "thinking" }, findings, seen);
}
}
if (Array.isArray(e.toolCalls)) {
for (const tc of e.toolCalls) scanToolCall(tc, compiledRules, ctx, findings, seen);
}
if (e.diffInfo) scanDiffInfo(e.diffInfo, compiledRules, ctx, findings, seen);
if (e.mcpInfo) scanMcpInfo(e.mcpInfo, compiledRules, ctx, findings, seen);
}
scanAgentHistory(session.agentHistory, compiledRules, findings, seen);
findings.sort((a, b) => {
const la = a.location;
const lb = b.location;
if (la.chapterIndex !== lb.chapterIndex) return la.chapterIndex - lb.chapterIndex;
if (la.eventIndex !== lb.eventIndex) return la.eventIndex - lb.eventIndex;
return (la.toolName || "").localeCompare(lb.toolName || "");
});
return findings;
}
function redactString(str, secrets) {
if (!str || typeof str !== "string") return str;
let out = str;
for (const secret of secrets) {
if (!secret || !out.includes(secret)) continue;
out = out.split(secret).join("[REDACTED]");
}
return out;
}
function redactValue(val, secrets) {
if (typeof val === "string") return redactPemBlocks(redactString(val, secrets));
if (Array.isArray(val)) return val.map((item) => redactValue(item, secrets));
if (val && typeof val === "object") {
for (const key of Object.keys(val)) {
val[key] = redactValue(val[key], secrets);
}
return val;
}
return val;
}
function redactPemBlocks(str) {
if (!str || typeof str !== "string") return str;
return str.replace(PEM_BLOCK_RE, "[REDACTED]");
}
function redactSession(session, findings) {
if (!findings.length) return structuredClone(session);
const secrets = [...new Set(findings.map((f) => f.secret).filter(Boolean))];
secrets.sort((a, b) => b.length - a.length);
const hasPemFinding = findings.some((f) => f.ruleId?.includes("private-key"));
const copy = redactValue(structuredClone(session), secrets);
if (hasPemFinding) return redactPemInTree(copy);
return copy;
}
const GH_LS_KEY = 'tracequest-gh-token';
const HF_LS_KEY = 'tracequest-hf-token';
const HF_REPO_LS_KEY = 'tracequest-hf-repo';
const HF_PREUPLOAD_THRESHOLD = 1048576;
function sanitizePathSegment(seg) {
var s = String(seg || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '-');
return s || 'unknown';
}
function escHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function isAllowedApexHost(hostname, apex) {
if (hostname === apex) return true;
var suffix = '.' + apex;
if (hostname.slice(-suffix.length) !== suffix) return false;
var sub = hostname.slice(0, -suffix.length);
return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(sub);
}
function buildGistHostPreviewUrl(gistId, filename) {
if (!gistId) return null;
var base = 'https://gisthost.github.io/?' + gistId;
if (filename && filename !== 'index.html') return base + '/' + filename;
return base;
}
function isAllowedShareUrl(url) {
try {
var u = new URL(url);
return u.protocol === 'https:' && (
u.hostname === 'gisthost.github.io' ||
isAllowedApexHost(u.hostname, 'gist.github.com') ||
isAllowedApexHost(u.hostname, 'huggingface.co')
);
} catch (e) { return false; }
}
function showShareSuccess(statusEl, url, secondaryUrl) {
if (!url || typeof url !== 'string') {
statusEl.className = 'hf-modal-status hf-error';
statusEl.textContent = 'Share succeeded but no preview URL was returned';
return;
}
statusEl.className = 'hf-modal-status hf-success';
statusEl.textContent = '';
statusEl.appendChild(document.createTextNode('Shared! '));
if (isAllowedShareUrl(url)) {
var link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.textContent = url;
statusEl.appendChild(link);
} else {
statusEl.appendChild(document.createTextNode(url));
}
if (secondaryUrl && isAllowedShareUrl(secondaryUrl)) {
statusEl.appendChild(document.createTextNode(' ('));
var secondaryLink = document.createElement('a');
secondaryLink.href = secondaryUrl;
secondaryLink.target = '_blank';
secondaryLink.rel = 'noopener noreferrer';
secondaryLink.textContent = 'raw gist';
statusEl.appendChild(secondaryLink);
statusEl.appendChild(document.createTextNode(')'));
}
var copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'hf-modal-cancel';
copyBtn.style.marginLeft = '8px';
copyBtn.textContent = 'Copy URL';
copyBtn.addEventListener('click', function() {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(function() {
copyBtn.textContent = 'Copied!';
}).catch(function() {});
}
});
statusEl.appendChild(copyBtn);
}
function shareGist(html, meta, token, isPrivate) {
var sessionId = meta.sessionId || 'session';
var source = meta.source || 'claude';
var filename = 'tracequest-' + sanitizePathSegment(source) + '-' + sanitizePathSegment(sessionId.slice(0, 8)) + '.html';
var description = source + ' session — ' + (meta.firstPrompt || '(no prompt)') + ' (' + (meta.model || 'unknown') + ', ' + (meta.durationFormatted || '—') + ')';
return fetch('https://api.github.com/gists', {
method: 'POST',
headers: {
Accept: 'application/vnd.github+json',
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json',
'X-GitHub-Api-Version': '2022-11-28',
},
body: JSON.stringify({
description: description,
public: !isPrivate,
files: { [filename]: { content: html } },
}),
}).then(function(res) {
if (!res.ok) return res.text().then(function(t) { throw new Error('Gist upload failed (' + res.status + '): ' + t); });
return res.json();
}).then(function(data) {
if (!data || !data.id) throw new Error('Gist upload succeeded but response missing id');
// gistUrl matches Node adapter url field (raw GitHub gist, not gisthost preview).
return {
previewUrl: buildGistHostPreviewUrl(data.id, filename),
gistUrl: data.html_url,
};
});
}
function hfPreuploadFile(namespace, repoName, path, content, token) {
var size = new Blob([content]).size;
var sample = content.slice(0, 512);
return fetch('https://huggingface.co/api/datasets/' + namespace + '/' + repoName + '/preupload/main', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ files: [{ path: path, size: size, sample: sample }] }),
}).then(function(res) {
if (!res.ok) return res.text().then(function(t) { throw new Error('HF preupload failed: ' + t); });
return res.json();
}).then(function(data) {
var info = data.files && data.files[0];
if (!info || !info.uploadUrl) throw new Error('HF preupload missing uploadUrl');
return fetch(info.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/octet-stream' },
body: content,
}).then(function(putRes) {
if (!putRes.ok) throw new Error('HF preupload PUT failed');
return { operation: 'addOrUpdate', path: path, oid: info.oid };
});
});
}
function hfFileOp(namespace, repoName, path, content, token) {
var size = new Blob([content]).size;
if (size >= HF_PREUPLOAD_THRESHOLD) {
return hfPreuploadFile(namespace, repoName, path, content, token);
}
return Promise.resolve({ operation: 'addOrUpdate', path: path, content: content });
}
function resolveHfRepo(token, repoInput) {
if (repoInput && repoInput.trim()) return Promise.resolve(repoInput.trim());
return fetch('https://huggingface.co/api/whoami-v2', {
headers: { Authorization: 'Bearer ' + token },
}).then(function(res) {
if (!res.ok) throw new Error('HF whoami failed');
return res.json();
}).then(function(data) {
var name = data.name || data.fullname;
if (!name) throw new Error('Could not resolve HF username');
return name + '/tracequest-sessions';
});
}
function shareHf(html, sidecar, token, repo, isPrivate) {
return resolveHfRepo(token, repo).then(function(targetRepo) {
var parts = targetRepo.split('/').filter(Boolean);
if (parts.length !== 2) throw new Error('HF repo must be namespace/repo');
var namespace = parts[0];
var repoName = parts[1];
var source = sanitizePathSegment(sidecar.source || 'claude');
var sessionId = sanitizePathSegment(sidecar.sessionId || 'session');
var htmlPath = 'sessions/' + source + '/' + sessionId + '.html';
var jsonPath = 'sessions/' + source + '/' + sessionId + '.json';
var readme = '---\nlicense: mit\ntags:\n- agent-traces\n- tracequest\ntask_categories:\n- text-generation\n---\n\n# tracequest sessions\n';
function commit(ops) {
return fetch('https://huggingface.co/api/datasets/' + namespace + '/' + repoName + '/commit/main', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ operations: ops }),
});
}
var username = namespace;
return fetch('https://huggingface.co/api/whoami-v2', {
headers: { Authorization: 'Bearer ' + token },
}).then(function(w) { return w.ok ? w.json() : {}; }).then(function(who) {
username = who.name || who.fullname || namespace;
return fetch('https://huggingface.co/api/datasets/' + namespace + '/' + repoName, {
headers: { Authorization: 'Bearer ' + token },
});
}).then(function(res) {
var isNew = !res.ok;
var chain = Promise.resolve();
if (isNew) {
var createBody = { name: repoName, type: 'dataset', private: !!isPrivate };
if (namespace !== username) createBody.organization = namespace;
chain = fetch('https://huggingface.co/api/repos/create', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify(createBody),
}).then(function(r) {
if (!r.ok) return r.text().then(function(t) { throw new Error('HF repo create failed: ' + t); });
});
}
return chain.then(function() {
return Promise.all([
hfFileOp(namespace, repoName, htmlPath, html, token),
hfFileOp(namespace, repoName, jsonPath, JSON.stringify(sidecar, null, 2), token),
]).then(function(ops) {
if (isNew) ops.push({ operation: 'addOrUpdate', path: 'README.md', content: readme });
return commit(ops);
});
}).then(function(r) {
if (!r.ok) return r.text().then(function(t) { throw new Error('HF commit failed: ' + t); });
return 'https://huggingface.co/datasets/' + namespace + '/' + repoName + '/blob/main/' + htmlPath;
});
});
});
}
function buildShareSidecar(sess) {
var chapters = typeof buildSessionChapters === 'function' ? buildSessionChapters(sess) : [];
var filePaths = [];
if (typeof chapterFileKeys === 'function') {
for (var i = 0; i < chapters.length; i++) {
var keys = chapterFileKeys(chapters[i]);
for (var j = 0; j < keys.length; j++) {
if (filePaths.indexOf(keys[j]) < 0) filePaths.push(keys[j]);
}
}
}
var stats = sess.stats || {};
var firstPrompt = '';
for (var k = 0; k < (sess.events || []).length; k++) {
if (sess.events[k].type === 'user' && sess.events[k].text) {
firstPrompt = typeof safeSlice === 'function' ? safeSlice(sess.events[k].text, FIRST_PROMPT_MAX_LEN) : sess.events[k].text.slice(0, 200);
break;
}
}
return {
sessionId: sess.sessionId,
source: sess.source || 'claude',
model: sess.model || '',
project: sess.cwd || '',
firstPrompt: firstPrompt,
eventCount: sess.eventCount || (sess.events ? sess.events.length : 0),
errorCount: stats.errors || 0,
tools: Object.keys(stats.toolCounts || {}),
filePaths: filePaths,
gitBranch: sess.gitBranch || null,
costEstimate: typeof estimateParsedStatsCost === 'function' ? estimateParsedStatsCost(sess.model, stats) : 0,
chapterCount: chapters.length,
};
}
function clientRenderHTML(redactedSession) {
var cssEl = document.querySelector('style');
var css = cssEl ? cssEl.textContent : '';
var scripts = document.getElementsByTagName('script');
var renderJs = '';
for (var i = 0; i < scripts.length; i++) {
var txt = scripts[i].textContent || '';
var marker = 'const SESSION = ';
var idx = txt.indexOf(marker);
if (idx >= 0) {
var after = txt.indexOf(';', idx);
if (after >= 0) renderJs = txt.slice(after + 1);
break;
}
}
var sid = escHtml((redactedSession.sessionId || 'session').slice(0, 8));
var data = JSON.stringify(redactedSession).replace(/<\//g, '<\\/');
return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>tracequest — ' + sid + '</title>\n<style>\n' + css + '\n</style>\n</head>\n<body>\n<div id="app"></div>\n<script>\nconst SESSION = ' + data + ';\n' + renderJs + '\n</scr' + 'ipt>\n</body>\n</html>';
}
var shareModalEl = null;
function closeShareModal() {
if (shareModalEl && shareModalEl.parentNode) shareModalEl.parentNode.removeChild(shareModalEl);
shareModalEl = null;
}
function openShareModal() {
closeShareModal();
var target = 'gist';
var overlay = h('div', { className: 'hf-modal-overlay' });
shareModalEl = overlay;
var modal = h('div', { className: 'hf-modal' });
modal.appendChild(h('div', { className: 'hf-modal-title' }, 'Share session'));
var targetRow = h('div', { style: 'display:flex;gap:12px;margin-bottom:8px;' });
var gistRadio = h('input', { type: 'radio', name: 'share-target' });
gistRadio.value = 'gist';
gistRadio.checked = true;
var hfRadio = h('input', { type: 'radio', name: 'share-target' });
hfRadio.value = 'hf';
targetRow.appendChild(h('label', null, gistRadio, ' Gist'));
targetRow.appendChild(h('label', null, hfRadio, ' HF'));
modal.appendChild(targetRow);
modal.appendChild(h('label', { className: 'hf-modal-label' }, 'Access token'));
var tokenInput = h('input', { className: 'hf-modal-input', type: 'password', placeholder: 'GitHub or Hugging Face token' });
tokenInput.value = localStorage.getItem(GH_LS_KEY) || '';
modal.appendChild(tokenInput);
modal.appendChild(h('div', { className: 'hf-modal-hint' }, 'Saved locally in your browser'));
modal.appendChild(h('label', { className: 'hf-modal-label' }, 'HF dataset repo (namespace/repo)'));
var repoInput = h('input', { className: 'hf-modal-input', type: 'text', placeholder: 'username/tracequest-sessions (optional — defaults via whoami)' });
repoInput.value = localStorage.getItem(HF_REPO_LS_KEY) || '';
repoInput.style.display = 'none';
modal.appendChild(repoInput);
var privateRow = h('label', { style: 'display:flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--fg2);' });
var privateChk = h('input', { type: 'checkbox' });
privateRow.appendChild(privateChk);
privateRow.appendChild(document.createTextNode('Private (secret gist / private HF repo)'));
modal.appendChild(privateRow);
var privateHint = h('div', { className: 'hf-modal-hint', style: 'display:none;margin-top:4px;' }, 'Secret gists: gisthost previews require GitHub sign-in; use the raw gist link if needed.');
modal.appendChild(privateHint);
var findingsPanel = h('div', { className: 'hf-modal-status', style: 'color:var(--yellow,#e0c45e);' });
findingsPanel.style.display = 'none';
modal.appendChild(findingsPanel);
var confirmRow = h('label', { style: 'display:none;align-items:center;gap:6px;margin-top:8px;font-size:12px;color:var(--fg2);' });
var confirmChk = h('input', { type: 'checkbox' });
confirmRow.appendChild(confirmChk);
confirmRow.appendChild(document.createTextNode('I understand — share anyway'));
modal.appendChild(confirmRow);
var statusEl = h('div', { className: 'hf-modal-status' });
modal.appendChild(statusEl);
var spinnerEl = h('span', { className: 'hf-modal-hint', style: 'display:none;margin-left:6px;' }, '⏳');
statusEl.appendChild(spinnerEl);
var actions = h('div', { className: 'hf-modal-actions' });
var cancelBtn = h('button', { className: 'hf-modal-cancel', type: 'button' }, 'Cancel');
var shareBtn = h('button', { className: 'hf-modal-share', type: 'button' }, 'Share');
actions.appendChild(cancelBtn);
actions.appendChild(shareBtn);
modal.appendChild(actions);
overlay.appendChild(modal);
function updatePrivateHint() {
privateHint.style.display = privateChk.checked && gistRadio.checked ? 'block' : 'none';
}
function updateTarget() {
target = gistRadio.checked ? 'gist' : 'hf';
repoInput.style.display = target === 'hf' ? 'block' : 'none';
tokenInput.value = localStorage.getItem(target === 'gist' ? GH_LS_KEY : HF_LS_KEY) || '';
updatePrivateHint();
scanPreview();
}
function scanPreview() {
var findings = scanSessionForSecrets(session, SECRET_RULES);
if (findings.length === 0) {
findingsPanel.style.display = 'none';
confirmRow.style.display = 'none';
shareBtn.disabled = false;
return;
}
findingsPanel.style.display = 'block';
confirmRow.style.display = 'flex';
findingsPanel.textContent = findings.length + ' potential secret(s) detected. Confirm to enable Share.';
shareBtn.disabled = !confirmChk.checked;
}
gistRadio.addEventListener('change', updateTarget);
hfRadio.addEventListener('change', updateTarget);
privateChk.addEventListener('change', updatePrivateHint);
confirmChk.addEventListener('change', function() { shareBtn.disabled = !confirmChk.checked; });
tokenInput.addEventListener('input', function() {
localStorage.setItem(target === 'gist' ? GH_LS_KEY : HF_LS_KEY, tokenInput.value);
});
repoInput.addEventListener('input', function() {
localStorage.setItem(HF_REPO_LS_KEY, repoInput.value);
});
cancelBtn.addEventListener('click', closeShareModal);
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) closeShareModal(); });
shareBtn.addEventListener('click', function() {
var token = tokenInput.value.trim();
if (!token) {
statusEl.className = 'hf-modal-status hf-error';
statusEl.textContent = 'Token required';
return;
}
localStorage.setItem(target === 'gist' ? GH_LS_KEY : HF_LS_KEY, token);
if (target === 'hf' && repoInput.value.trim()) {
localStorage.setItem(HF_REPO_LS_KEY, repoInput.value.trim());
}
var findings = scanSessionForSecrets(session, SECRET_RULES);
if (findings.length && !confirmChk.checked) {
statusEl.className = 'hf-modal-status hf-error';
statusEl.textContent = 'Confirm sharing with detected secrets';
return;
}
shareBtn.disabled = true;
statusEl.className = 'hf-modal-status';
statusEl.textContent = 'Uploading…';
spinnerEl.style.display = 'inline';
var redacted = redactSession(session, findings);
var html = clientRenderHTML(redacted);
var sidecar = buildShareSidecar(redacted);
sidecar.firstPrompt = sidecar.firstPrompt || '';
var meta = {
sessionId: redacted.sessionId,
source: redacted.source,
model: redacted.model,
firstPrompt: sidecar.firstPrompt,
durationFormatted: redacted.durationMs > 0 && typeof formatDuration === 'function' ? formatDuration(redacted.durationMs) : '—',
};
var upload = target === 'gist'
? shareGist(html, meta, token, privateChk.checked)
: shareHf(html, sidecar, token, repoInput.value.trim(), privateChk.checked);
upload.then(function(result) {
spinnerEl.style.display = 'none';
var previewUrl = typeof result === 'string' ? result : (result && result.previewUrl) || null;
var gistUrl = result && result.gistUrl ? result.gistUrl : null;
showShareSuccess(statusEl, previewUrl, gistUrl);
shareBtn.disabled = false;
shareBtn.textContent = 'Share';
}).catch(function(err) {
spinnerEl.style.display = 'none';
statusEl.className = 'hf-modal-status hf-error';
statusEl.textContent = err && err.message ? err.message : String(err);
shareBtn.disabled = false;
});
});
document.body.appendChild(overlay);
updateTarget();
}
// Read URL state before first render
readUrlState();
render();
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment