agent-trace 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/agent-trace.cjs +551 -398
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24008,439 +24008,579 @@ var import_node_fs5 = __toESM(require("node:fs"));
24008
24008
  var import_node_http = __toESM(require("node:http"));
24009
24009
 
24010
24010
  // packages/dashboard/src/web-render.ts
24011
- function escapeHtml(value) {
24012
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
24013
- }
24014
24011
  function renderDashboardHtml(options = {}) {
24015
24012
  const title = options.title ?? "agent-trace dashboard";
24016
- const safeTitle = escapeHtml(title);
24017
24013
  return `<!doctype html>
24018
24014
  <html lang="en">
24019
- <head>
24020
- <meta charset="utf-8" />
24021
- <meta name="viewport" content="width=device-width, initial-scale=1" />
24022
- <title>${safeTitle}</title>
24023
- <style>
24024
- :root {
24025
- --paper: #f7f7f2;
24026
- --ink: #121212;
24027
- --accent: #0f766e;
24028
- --accent-soft: #d1fae5;
24029
- --grid: #d6d3d1;
24030
- --warn: #9a3412;
24031
- }
24032
-
24033
- * {
24034
- box-sizing: border-box;
24035
- }
24036
-
24037
- body {
24038
- margin: 0;
24039
- color: var(--ink);
24040
- font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
24041
- background:
24042
- radial-gradient(circle at 20% 10%, rgba(15, 118, 110, 0.18), transparent 30%),
24043
- radial-gradient(circle at 80% 0%, rgba(217, 119, 6, 0.15), transparent 35%),
24044
- linear-gradient(145deg, #f5f5f4, var(--paper));
24045
- }
24046
-
24047
- .shell {
24048
- max-width: 1100px;
24049
- margin: 0 auto;
24050
- padding: 28px 18px 40px;
24051
- }
24052
-
24053
- .hero {
24054
- border: 2px solid var(--ink);
24055
- background: #fff;
24056
- box-shadow: 8px 8px 0 var(--ink);
24057
- padding: 22px 20px;
24058
- }
24059
-
24060
- .hero h1 {
24061
- margin: 0;
24062
- font-size: clamp(1.7rem, 2.4vw, 2.3rem);
24063
- letter-spacing: -0.02em;
24064
- }
24065
-
24066
- .hero p {
24067
- margin: 10px 0 0;
24068
- color: #3f3f46;
24069
- }
24070
-
24071
- .grid {
24072
- margin-top: 18px;
24073
- display: grid;
24074
- grid-template-columns: repeat(3, minmax(0, 1fr));
24075
- gap: 12px;
24076
- }
24015
+ <head>
24016
+ <meta charset="utf-8"/>
24017
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
24018
+ <title>${title}</title>
24019
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark-dimmed.min.css"/>
24020
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
24021
+ <style>
24022
+ :root{--bg:#000;--panel:#0a0a0a;--panel-border:#1a1a1a;--panel-hover:#111;--panel-muted:#060606;--text-primary:#d4d4d4;--text-muted:#666;--text-dim:#444;--line:#1a1a1a;--green:#4ade80;--green-dim:rgba(74,222,128,.12);--orange:#fb923c;--orange-dim:rgba(251,146,60,.12);--red:#f87171;--red-dim:rgba(248,113,113,.12);--purple:#c084fc;--purple-dim:rgba(192,132,252,.1);--cyan:#22d3ee;--cyan-dim:rgba(34,211,238,.08);--yellow:#facc15}
24023
+ *{box-sizing:border-box}
24024
+ html,body{margin:0;padding:0;min-height:100vh;background:var(--bg);color:var(--text-primary);font-family:"SF Mono","JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:13px;-webkit-font-smoothing:antialiased}
24025
+ .shell{max-width:1400px;margin:0 auto;padding:20px 16px 40px}
24026
+ .hero{border:1px solid var(--panel-border);border-radius:8px;padding:16px;background:var(--panel)}
24027
+ .hero h1{margin:0;font-size:16px;font-weight:600;letter-spacing:-.02em}
24028
+ .hero p{margin:4px 0 0;font-size:12px;color:var(--text-muted)}
24029
+ .status-banner{margin-top:10px;padding:8px 10px;border-radius:6px;border:1px solid var(--line);background:var(--panel-muted);color:var(--text-muted);font-size:12px}
24030
+ .status-banner.warning{border-color:rgba(250,204,21,.4);color:var(--yellow)}
24031
+ .mg{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(5,minmax(0,1fr))}
24032
+ .mc{border:1px solid var(--panel-border);border-radius:8px;padding:10px 12px;background:var(--panel)}
24033
+ .mc .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
24034
+ .mc .val{margin-top:6px;font-size:18px;font-weight:700;font-variant-numeric:tabular-nums}
24035
+ .mc .det{font-size:10px;color:var(--text-dim);margin-top:2px}
24036
+ .green{color:var(--green)}.cyan{color:var(--cyan)}.orange{color:var(--orange)}.red{color:var(--red)}.purple{color:var(--purple)}.yellow{color:var(--yellow)}
24037
+ .sg{margin-top:14px;display:grid;gap:10px;grid-template-columns:1.2fr .8fr}
24038
+ .panel{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden}
24039
+ .ph{padding:10px 12px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
24040
+ .ph h2{margin:0;font-size:13px;font-weight:600;letter-spacing:-.01em}
24041
+ .ph p{margin:2px 0 0;font-size:11px;color:var(--text-dim)}
24042
+ .pc{padding:8px}
24043
+ table{width:100%;border-collapse:collapse}
24044
+ th,td{border-bottom:1px solid var(--line);text-align:left;padding:7px 8px;font-size:12px}
24045
+ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:500}
24046
+ .srow{cursor:pointer}.srow:hover{background:var(--panel-hover)}.srow.active{background:var(--green-dim)}
24047
+ .badge{display:inline-flex;align-items:center;border-radius:4px;padding:1px 6px;font-size:11px;border:1px solid var(--line);color:var(--text-muted);font-variant-numeric:tabular-nums}
24048
+ .badge.green{border-color:rgba(74,222,128,.3);color:var(--green)}
24049
+ .badge.orange{border-color:rgba(251,146,60,.3);color:var(--orange)}
24050
+ .badge.red{border-color:rgba(248,113,113,.3);color:var(--red)}
24051
+ .badge.purple{border-color:rgba(192,132,252,.3);color:var(--purple)}
24052
+ .badge.cyan{border-color:rgba(34,211,238,.3);color:var(--cyan)}
24053
+ .badge.dim{border-color:var(--line);color:var(--text-dim)}
24054
+ .badge.commit{border-color:rgba(250,204,21,.3);color:var(--yellow)}
24055
+ .ls{font-size:11px;font-variant-numeric:tabular-nums}
24056
+ .ls.green{color:var(--green);margin-right:4px}.ls.red{color:var(--red)}
24057
+ .repo-cell{max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
24058
+ .tm{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
24059
+ .tmi{border:1px solid var(--line);border-radius:6px;padding:4px 8px;font-size:11px;color:var(--text-muted)}
24060
+ .chart{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;align-items:end;min-height:160px}
24061
+ .chart-col{display:flex;flex-direction:column;align-items:stretch;justify-content:end;gap:4px}
24062
+ .chart-bar{width:100%;border-radius:4px 4px 1px 1px;background:linear-gradient(180deg,var(--green),#166534);min-height:3px}
24063
+ .chart-label{font-size:10px;color:var(--text-dim);text-align:center}
24064
+ .chart-value{font-size:11px;color:var(--text-muted);text-align:center;font-variant-numeric:tabular-nums}
24065
+ .empty{padding:12px;color:var(--text-dim);font-size:12px}
24066
+ .pg{border:1px solid var(--panel-border);border-radius:8px;margin-bottom:8px;background:var(--panel-muted);overflow:hidden}
24067
+ .pg.expanded{border-color:#222}
24068
+ .pg-hd{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;cursor:pointer;user-select:none}
24069
+ .pg-hd:hover{background:var(--panel-hover)}
24070
+ .pg-idx{flex-shrink:0;width:28px;height:22px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:var(--green-dim);color:var(--green);border:1px solid rgba(74,222,128,.2)}
24071
+ .pg-txt{flex:1;min-width:0;font-size:13px;color:var(--text-primary);line-height:1.5;white-space:pre-wrap;word-break:break-word}
24072
+ .pg-txt.trunc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
24073
+ .pg-stats{flex-shrink:0;display:flex;gap:6px;align-items:center;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
24074
+ .pg-arrow{flex-shrink:0;font-size:12px;color:var(--text-dim);transition:transform .15s}
24075
+ .pg-arrow.open{transform:rotate(90deg)}
24076
+ .pg-body{border-top:1px solid var(--line)}
24077
+ .pg-full{padding:10px 12px;border-bottom:1px solid var(--line);background:rgba(74,222,128,.04)}
24078
+ .pg-full-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px}
24079
+ .pg-full-content{font-size:12px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto}
24080
+ .erow{display:grid;grid-template-columns:18px 1fr auto;gap:8px;align-items:start;padding:6px 12px;border-bottom:1px solid var(--line);font-size:12px;min-height:32px}
24081
+ .erow:last-child{border-bottom:none}
24082
+ .eicon{width:18px;height:18px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0;margin-top:1px}
24083
+ .eicon.tool{background:var(--purple-dim);color:var(--purple);border:1px solid rgba(192,132,252,.2)}
24084
+ .eicon.api{background:var(--cyan-dim);color:var(--cyan);border:1px solid rgba(34,211,238,.15)}
24085
+ .eicon.error{background:var(--red-dim);color:var(--red);border:1px solid rgba(248,113,113,.2)}
24086
+ .econtent{min-width:0}
24087
+ .elabel{color:var(--text-primary);font-weight:500}
24088
+ .edetail{margin-top:2px;color:var(--text-muted);font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
24089
+ .emeta{display:flex;gap:6px;align-items:center;flex-shrink:0;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
24090
+ .rblock{padding:10px 12px;border-top:1px solid var(--line);background:rgba(74,222,128,.03)}
24091
+ .rblock-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px}
24092
+ .rblock-text{font-size:12px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto}
24093
+ .tool-fp{display:inline-block;font-size:11px;color:var(--cyan);padding:1px 5px;border-radius:3px;background:var(--cyan-dim);margin-bottom:4px}
24094
+ .tool-pat{display:inline-block;font-size:11px;color:var(--purple);padding:1px 5px;border-radius:3px;background:var(--purple-dim);margin-right:4px}
24095
+ .cblock{border:1px solid var(--line);border-radius:6px;overflow:hidden;margin-top:4px;margin-bottom:4px;background:#050505}
24096
+ .cblock-hd{padding:3px 8px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);background:#0d0d0d;border-bottom:1px solid var(--line)}
24097
+ .cblock pre{margin:0;padding:8px 10px;font-size:11px;line-height:1.6;color:var(--text-primary);overflow-x:auto;max-height:300px;overflow-y:auto;white-space:pre;tab-size:2}
24098
+ .cblock pre code{font-family:inherit}
24099
+ .diff-block{border:1px solid var(--line);border-radius:6px;overflow:hidden;margin-top:4px;margin-bottom:4px;background:#050505}
24100
+ .diff-rm{display:flex;border-bottom:1px solid var(--line);background:rgba(248,113,113,.06)}
24101
+ .diff-add{display:flex;background:rgba(74,222,128,.06)}
24102
+ .diff-lbl{flex-shrink:0;width:24px;padding:6px 0;text-align:center;font-size:11px;font-weight:700;user-select:none}
24103
+ .diff-rm .diff-lbl{color:var(--red)}.diff-add .diff-lbl{color:var(--green)}
24104
+ .diff-rm pre,.diff-add pre{margin:0;padding:6px 8px;font-size:11px;line-height:1.5;color:var(--text-primary);overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre;tab-size:2;flex:1;min-width:0}
24105
+ .fsummary{padding:8px 12px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:4px}
24106
+ .fsg{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
24107
+ .fsg-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 5px;border-radius:3px;font-weight:600}
24108
+ .fsg-label.written{color:var(--green);background:var(--green-dim)}
24109
+ .fsg-label.read{color:var(--text-muted);background:rgba(255,255,255,.04)}
24110
+ .fsg-path{font-size:11px;color:var(--text-muted);padding:1px 5px;border-radius:3px;background:rgba(255,255,255,.03)}
24111
+ .outcome{border:1px solid var(--panel-border);border-radius:6px;padding:10px 12px;margin-bottom:12px;background:var(--panel-muted)}
24112
+ .outcome-hd{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}
24113
+ .outcome-row{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:8px}
24114
+ .outcome-item{display:flex;align-items:center;gap:4px}
24115
+ .outcome-lbl{font-size:11px;color:var(--text-dim)}
24116
+ .outcome-val{font-size:12px;color:var(--text-primary)}
24117
+ .outcome-commits{border-top:1px solid var(--panel-border);padding-top:6px;margin-top:4px}
24118
+ .outcome-cr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
24119
+ .commit-sha{color:var(--yellow);font-weight:600;min-width:56px}
24120
+ .commit-msg{color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
24121
+ .commit-pl{font-size:10px;color:var(--text-dim)}
24122
+ .outcome-prs{border-top:1px solid var(--panel-border);padding-top:6px;margin-top:4px}
24123
+ .outcome-pr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
24124
+ .pr-badge{font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;text-transform:uppercase;border:1px solid rgba(74,222,128,.3);color:var(--green)}
24125
+ .pr-label{color:var(--cyan);font-weight:600}
24126
+ .pr-repo{color:var(--text-muted)}
24127
+ .pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
24128
+ .pr-link:hover{color:var(--cyan);text-decoration:underline}
24129
+ .pcommits{border:1px solid rgba(250,204,21,.15);border-radius:4px;padding:6px 8px;margin-bottom:8px;background:rgba(250,204,21,.04)}
24130
+ .pcommit{display:flex;align-items:center;gap:8px;padding:2px 0;font-size:12px}
24131
+ .hljs{background:transparent!important;color:var(--text-primary)}
24132
+ .hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-name{color:#c084fc}
24133
+ .hljs-string,.hljs-attr{color:#4ade80}
24134
+ .hljs-number,.hljs-literal{color:#fb923c}
24135
+ .hljs-comment{color:#555;font-style:italic}
24136
+ .hljs-type,.hljs-class .hljs-title,.hljs-title.class_{color:#22d3ee}
24137
+ .hljs-function .hljs-title,.hljs-title.function_{color:#60a5fa}
24138
+ .hljs-variable,.hljs-template-variable{color:#f87171}
24139
+ .hljs-regexp{color:#fbbf24}
24140
+ .hljs-meta{color:#666}
24141
+ .hljs-punctuation{color:#888}
24142
+ .hljs-property{color:#93c5fd}
24143
+ .hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
24144
+ .hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
24145
+ @media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}}
24146
+ @media(max-width:760px){.shell{padding:12px 8px 24px}.mg{grid-template-columns:1fr}th:nth-child(5),td:nth-child(5),th:nth-child(7),td:nth-child(7){display:none}.erow{grid-template-columns:18px 1fr}.emeta{grid-column:2}.pg-stats{display:none}}
24147
+ </style>
24148
+ </head>
24149
+ <body>
24150
+ <main class="shell">
24151
+ <section class="hero">
24152
+ <h1>${title}</h1>
24153
+ <p>session observability for coding agents</p>
24154
+ <div id="status" class="status-banner">Connecting...</div>
24155
+ </section>
24156
+ <section class="mg">
24157
+ <article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
24158
+ <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
24159
+ <article class="mc"><div class="label">Prompts</div><div class="val cyan" id="m-prompts">0</div></article>
24160
+ <article class="mc"><div class="label">Tool Calls</div><div class="val" id="m-tools">0</div></article>
24161
+ <article class="mc"><div class="label">Commits</div><div class="val green" id="m-commits">0</div><div class="det" id="m-commits-det"></div></article>
24162
+ </section>
24163
+ <section class="sg">
24164
+ <section class="panel">
24165
+ <header class="ph"><div><h2>Sessions</h2><p id="stream-label">...</p></div></header>
24166
+ <div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
24167
+ </section>
24168
+ <section class="panel">
24169
+ <header class="ph"><div><h2>Daily Cost</h2><p>7-day spend</p></div></header>
24170
+ <div class="pc"><div id="cost-chart" class="empty">Loading...</div></div>
24171
+ </section>
24172
+ </section>
24173
+ <section class="panel" style="margin-top:10px">
24174
+ <header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
24175
+ <div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
24176
+ </section>
24177
+ </main>
24178
+ <script>
24179
+ (function(){
24180
+ var DQ = String.fromCharCode(34);
24181
+ var selectedId = null;
24182
+ var sessions = [];
24183
+ var costPoints = [];
24184
+ var replay = null;
24077
24185
 
24078
- .metric {
24079
- border: 1px solid var(--grid);
24080
- background: #fff;
24081
- padding: 12px;
24082
- }
24186
+ function esc(s) {
24187
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(new RegExp(DQ,'g'),'&quot;');
24188
+ }
24189
+ function fmt$(v) { return '$' + v.toFixed(2); }
24190
+ function fmt$4(v) { return '$' + v.toFixed(4); }
24191
+ function ensureUtc(v) {
24192
+ if (v.endsWith('Z') || /[+-]\\d{2}:\\d{2}$/.test(v)) return v;
24193
+ return v.includes('T') ? v + 'Z' : v.replace(' ','T') + 'Z';
24194
+ }
24195
+ function fmtDate(v) { try { return new Date(ensureUtc(v)).toLocaleString(); } catch(e) { return v; } }
24196
+ function fmtTime(v) { try { return new Date(ensureUtc(v)).toLocaleTimeString(); } catch(e) { return v; } }
24197
+ function fmtDur(ms) { return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's'; }
24083
24198
 
24084
- .metric .label {
24085
- font-size: 0.78rem;
24086
- text-transform: uppercase;
24087
- letter-spacing: 0.08em;
24088
- color: #52525b;
24089
- }
24199
+ function readStr(r, k) { var v = r[k]; return typeof v === 'string' && v.length > 0 ? v : undefined; }
24200
+ function readNum(r, k) { var v = r[k]; return typeof v === 'number' && isFinite(v) ? v : undefined; }
24201
+ function readArr(r, k) { var v = r[k]; return Array.isArray(v) ? v : []; }
24202
+ function asRec(v) { return typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined; }
24090
24203
 
24091
- .metric .value {
24092
- margin-top: 6px;
24093
- font-size: 1.35rem;
24094
- font-weight: 700;
24095
- }
24204
+ var EXT_MAP = {ts:'typescript',tsx:'typescript',js:'javascript',jsx:'javascript',py:'python',rb:'ruby',go:'go',rs:'rust',java:'java',css:'css',html:'html',json:'json',yaml:'yaml',yml:'yaml',md:'markdown',sh:'bash',bash:'bash',sql:'sql',toml:'yaml'};
24205
+ function guessLang(fp) { var e = (fp.split('.').pop()||'').toLowerCase(); return EXT_MAP[e] || e; }
24206
+ function highlight(code, lang) {
24207
+ if (typeof hljs === 'undefined') return esc(code);
24208
+ try { var r = hljs.highlight(code, {language: lang || 'text', ignoreIllegals: true}); return r.value; } catch(e) { return esc(code); }
24209
+ }
24096
24210
 
24097
- .panel {
24098
- margin-top: 18px;
24099
- border: 2px solid var(--ink);
24100
- background: #fff;
24101
- box-shadow: 8px 8px 0 var(--ink);
24102
- overflow: hidden;
24103
- }
24211
+ var READ_TOOLS = {Read:1,Glob:1,Grep:1,Search:1};
24212
+ var WRITE_TOOLS = {Write:1,Edit:1,NotebookEdit:1};
24104
24213
 
24105
- .panel header {
24106
- padding: 12px 14px;
24107
- background: var(--accent-soft);
24108
- border-bottom: 2px solid var(--ink);
24109
- font-weight: 700;
24110
- }
24214
+ function parseReplay(val) {
24215
+ var r = asRec(val); if (!r) return null;
24216
+ var sid = readStr(r,'sessionId'), sa = readStr(r,'startedAt'), m = asRec(r.metrics), tl = r.timeline;
24217
+ if (!sid || !sa || !m || !Array.isArray(tl)) return null;
24218
+ var envR = asRec(r.environment), gitR = asRec(r.git);
24219
+ var gitBranch = (envR ? readStr(envR,'gitBranch') : undefined) || readStr(r,'gitBranch');
24220
+ var cRaw = (gitR && Array.isArray(gitR.commits)) ? gitR.commits : Array.isArray(r.commits) ? r.commits : [];
24221
+ var commits = cRaw.map(function(e){var c=asRec(e);if(!c)return null;var sha=readStr(c,'sha');if(!sha||sha.indexOf('placeholder_')===0)return null;return{sha:sha,message:readStr(c,'message'),promptId:readStr(c,'promptId'),committedAt:readStr(c,'committedAt')};}).filter(Boolean);
24222
+ var pRaw = (gitR && Array.isArray(gitR.pullRequests)) ? gitR.pullRequests : Array.isArray(r.pullRequests) ? r.pullRequests : [];
24223
+ var prs = pRaw.map(function(e){var p=asRec(e);if(!p)return null;var repo=readStr(p,'repo'),n=readNum(p,'prNumber');if(!repo||n===undefined)return null;return{repo:repo,prNumber:n,state:readStr(p,'state')||'open',url:readStr(p,'url')};}).filter(Boolean);
24224
+ var timeline = tl.map(function(e){
24225
+ var ev=asRec(e);if(!ev)return null;
24226
+ var id=readStr(ev,'id'),type=readStr(ev,'type'),ts=readStr(ev,'timestamp');if(!id||!type||!ts)return null;
24227
+ var d=asRec(ev.details),tok=asRec(ev.tokens);
24228
+ return{id:id,type:type,timestamp:ts,promptId:readStr(ev,'promptId'),status:readStr(ev,'status'),costUsd:readNum(ev,'costUsd'),toolName:d?readStr(d,'toolName'):undefined,toolDurationMs:d?readNum(d,'toolDurationMs'):undefined,inputTokens:tok?readNum(tok,'input'):undefined,outputTokens:tok?readNum(tok,'output'):undefined,details:d};
24229
+ }).filter(Boolean);
24230
+ return{sessionId:sid,startedAt:sa,endedAt:readStr(r,'endedAt'),gitBranch:gitBranch,
24231
+ metrics:{promptCount:readNum(m,'promptCount')||0,toolCallCount:readNum(m,'toolCallCount')||0,totalCostUsd:readNum(m,'totalCostUsd')||0,totalInputTokens:readNum(m,'totalInputTokens')||0,totalOutputTokens:readNum(m,'totalOutputTokens')||0,linesAdded:readNum(m,'linesAdded')||0,linesRemoved:readNum(m,'linesRemoved')||0,modelsUsed:readArr(m,'modelsUsed').filter(function(x){return typeof x==='string'}),toolsUsed:readArr(m,'toolsUsed').filter(function(x){return typeof x==='string'}),filesTouched:readArr(m,'filesTouched').filter(function(x){return typeof x==='string'})},
24232
+ commits:commits,pullRequests:prs,timeline:timeline};
24233
+ }
24111
24234
 
24112
- table {
24113
- width: 100%;
24114
- border-collapse: collapse;
24115
- }
24235
+ function extractToolDetail(ev) {
24236
+ var d = ev.details, tn = ev.toolName || ev.type;
24237
+ if (!d) return {toolName:tn};
24238
+ var rawTi = d.toolInput || d.tool_input, inp = asRec(rawTi), tiStr = typeof rawTi === 'string' ? rawTi : undefined;
24239
+ function rs(k) { return (d ? readStr(d,k) : undefined) || (inp ? readStr(inp,k) : undefined) || (tiStr ? extractFromTruncJson(tiStr,k) : undefined); }
24240
+ return {toolName:tn,filePath:rs('filePath')||rs('file_path'),command:rs('command')||rs('cmd'),pattern:rs('pattern'),oldString:(d?readStr(d,'oldString'):undefined)||(inp?readStr(inp,'old_string'):undefined),newString:(d?readStr(d,'newString'):undefined)||(inp?readStr(inp,'new_string'):undefined),writeContent:d?readStr(d,'writeContent'):undefined,description:rs('description')};
24241
+ }
24116
24242
 
24117
- th, td {
24118
- padding: 10px 12px;
24119
- border-bottom: 1px solid var(--grid);
24120
- text-align: left;
24121
- font-size: 0.95rem;
24122
- }
24243
+ function extractFromTruncJson(raw, key) {
24244
+ var re = new RegExp(DQ + key + DQ + '\\\\s*:\\\\s*' + DQ + '([^' + DQ + ']*?)' + DQ);
24245
+ var m = re.exec(raw); return m && m[1] && m[1].length > 0 ? m[1] : undefined;
24246
+ }
24123
24247
 
24124
- th {
24125
- font-size: 0.8rem;
24126
- text-transform: uppercase;
24127
- letter-spacing: 0.06em;
24128
- color: #57534e;
24129
- background: #fafaf9;
24130
- }
24248
+ function renderToolDetail(ev) {
24249
+ var td = extractToolDetail(ev);
24250
+ if (td.toolName === 'Bash' && td.command) return '<div class="cblock"><div class="cblock-hd">bash</div><pre><code class="language-bash">' + highlight(td.command,'bash') + '</code></pre></div>';
24251
+ if (td.toolName === 'Edit' && td.filePath) {
24252
+ var h = '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24253
+ if (td.oldString) {
24254
+ h += '<div class="diff-block"><div class="diff-rm"><div class="diff-lbl">-</div><pre><code>' + esc(td.oldString) + '</code></pre></div>';
24255
+ if (td.newString) h += '<div class="diff-add"><div class="diff-lbl">+</div><pre><code>' + esc(td.newString) + '</code></pre></div>';
24256
+ h += '</div>';
24257
+ }
24258
+ return h;
24259
+ }
24260
+ if ((td.toolName === 'Grep' || td.toolName === 'Glob') && td.pattern) {
24261
+ var h2 = '<span class="tool-pat">' + esc(td.toolName === 'Grep' ? '/' + td.pattern + '/' : td.pattern) + '</span>';
24262
+ if (td.filePath) h2 += ' <span class="tool-fp">' + esc(td.filePath) + '</span>';
24263
+ return h2;
24264
+ }
24265
+ if (td.toolName === 'Task' && td.description) return '<span class="edetail">' + esc(td.description) + '</span>';
24266
+ if (td.toolName === 'Write' && td.filePath) {
24267
+ var lang = guessLang(td.filePath);
24268
+ var h3 = '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24269
+ if (td.writeContent) h3 += '<div class="cblock"><div class="cblock-hd">' + esc(lang) + '</div><pre><code class="language-' + esc(lang) + '">' + highlight(td.writeContent,lang) + '</code></pre></div>';
24270
+ return h3;
24271
+ }
24272
+ if (td.filePath) return '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24273
+ if (td.command) return '<div class="cblock"><div class="cblock-hd">shell</div><pre><code class="language-bash">' + highlight(td.command,'bash') + '</code></pre></div>';
24274
+ return '';
24275
+ }
24131
24276
 
24132
- .status {
24133
- margin-top: 12px;
24134
- padding: 10px 12px;
24135
- border: 1px solid var(--grid);
24136
- background: #fff;
24137
- }
24277
+ function renderEventRow(ev) {
24278
+ var iconCls = ev.status === 'error' ? 'eicon error' : ev.toolName ? 'eicon tool' : 'eicon api';
24279
+ var iconChr = ev.status === 'error' ? '!' : ev.toolName ? 'T' : 'E';
24280
+ var meta = '';
24281
+ if (ev.toolDurationMs !== undefined) meta += '<span class="badge">' + fmtDur(ev.toolDurationMs) + '</span>';
24282
+ if (ev.costUsd !== undefined && ev.costUsd > 0) meta += '<span class="badge orange">' + fmt$4(ev.costUsd) + '</span>';
24283
+ if (ev.status) {
24284
+ var sc = ev.status === 'error' ? 'red' : (ev.status === 'ok' || ev.status === 'success') ? 'green' : '';
24285
+ meta += '<span class="badge ' + sc + '">' + esc(ev.status) + '</span>';
24286
+ }
24287
+ meta += '<span style="color:var(--text-dim)">' + fmtTime(ev.timestamp) + '</span>';
24288
+ return '<div class="erow"><div class="' + iconCls + '">' + iconChr + '</div><div class="econtent"><div class="elabel">' + esc(ev.toolName || ev.type) + '</div>' + renderToolDetail(ev) + '</div><div class="emeta">' + meta + '</div></div>';
24289
+ }
24138
24290
 
24139
- .status.error {
24140
- color: var(--warn);
24141
- border-color: #fca5a5;
24142
- background: #fef2f2;
24291
+ function buildPromptGroups(timeline, commits) {
24292
+ var byPrompt = {};
24293
+ commits.forEach(function(c){ if(c.promptId){if(!byPrompt[c.promptId])byPrompt[c.promptId]=[];byPrompt[c.promptId].push(c);} });
24294
+ var order = [], map = {};
24295
+ timeline.forEach(function(ev){
24296
+ if(!ev.promptId) return;
24297
+ if(!map[ev.promptId]){order.push(ev.promptId);map[ev.promptId]=[];}
24298
+ map[ev.promptId].push(ev);
24299
+ });
24300
+ return order.map(function(pid){
24301
+ var evts = map[pid] || [], promptText, responseText, cost=0,tools=0,inTok=0,outTok=0,dur=0;
24302
+ var filesR={},filesW={},toolEvts=[];
24303
+ evts.forEach(function(ev){
24304
+ var d = ev.details;
24305
+ if(!promptText && d){var pt=readStr(d,'promptText');if(pt)promptText=pt;}
24306
+ if(ev.type==='assistant_response'||ev.type==='api_call'||ev.type==='api_response'){
24307
+ if(d){var rt=readStr(d,'responseText')||readStr(d,'lastAssistantMessage');if(rt)responseText=rt;}
24308
+ }
24309
+ cost+=(ev.costUsd||0);inTok+=(ev.inputTokens||0);outTok+=(ev.outputTokens||0);
24310
+ if(ev.toolName||(ev.type==='tool_call'||ev.type==='tool_result')){
24311
+ toolEvts.push(ev);tools++;dur+=(ev.toolDurationMs||0);
24312
+ var dd=ev.details,rti=dd?(dd.toolInput||dd.tool_input):undefined,inp=asRec(rti),tiStr=typeof rti==='string'?rti:undefined;
24313
+ var fp=(dd?readStr(dd,'filePath'):undefined)||(inp?readStr(inp,'file_path'):undefined)||(tiStr?extractFromTruncJson(tiStr,'file_path'):undefined);
24314
+ if(fp){var tn=ev.toolName||'';if(WRITE_TOOLS[tn])filesW[fp]=1;else if(READ_TOOLS[tn])filesR[fp]=1;}
24143
24315
  }
24316
+ });
24317
+ // deduplicate tool events
24318
+ var deduped = [];
24319
+ toolEvts.forEach(function(ev){
24320
+ if(deduped.length>0){
24321
+ var prev=deduped[deduped.length-1];
24322
+ if(prev.toolName===ev.toolName){
24323
+ var pfp=prev.details?(readStr(prev.details,'filePath')||readStr(asRec(prev.details.toolInput)||{},'file_path')):undefined;
24324
+ var cfp=ev.details?(readStr(ev.details,'filePath')||readStr(asRec(ev.details.toolInput)||{},'file_path')):undefined;
24325
+ if(pfp&&pfp===cfp)return;
24326
+ }
24327
+ }
24328
+ deduped.push(ev);
24329
+ });
24330
+ return{promptId:pid,promptText:promptText,responseText:responseText,toolEvents:deduped,commits:byPrompt[pid]||[],totalCostUsd:cost,totalToolCalls:tools,totalInputTokens:inTok,totalOutputTokens:outTok,totalDurationMs:dur,filesRead:Object.keys(filesR),filesWritten:Object.keys(filesW)};
24331
+ }).filter(function(g){return g.promptText||g.toolEvents.length>0||g.responseText;});
24332
+ }
24144
24333
 
24145
- @media (max-width: 840px) {
24146
- .grid {
24147
- grid-template-columns: 1fr;
24148
- }
24149
-
24150
- th:nth-child(4),
24151
- td:nth-child(4) {
24152
- display: none;
24153
- }
24154
- }
24155
- </style>
24156
- </head>
24157
- <body>
24158
- <main class="shell">
24159
- <section class="hero">
24160
- <h1>${safeTitle}</h1>
24161
- <p>Session-level observability for coding agents, running locally.</p>
24162
- </section>
24163
-
24164
- <section class="grid">
24165
- <article class="metric">
24166
- <div class="label">Sessions</div>
24167
- <div class="value" id="metric-sessions">0</div>
24168
- </article>
24169
- <article class="metric">
24170
- <div class="label">Total Cost (USD)</div>
24171
- <div class="value" id="metric-cost">$0.00</div>
24172
- </article>
24173
- <article class="metric">
24174
- <div class="label">Latest Start</div>
24175
- <div class="value" id="metric-latest">-</div>
24176
- </article>
24177
- </section>
24178
-
24179
- <section class="panel">
24180
- <header>Recent Sessions</header>
24181
- <table>
24182
- <thead>
24183
- <tr>
24184
- <th>Session</th>
24185
- <th>User</th>
24186
- <th>Repo</th>
24187
- <th>Started</th>
24188
- <th>Cost</th>
24189
- <th>Replay</th>
24190
- </tr>
24191
- </thead>
24192
- <tbody id="sessions-body">
24193
- <tr><td colspan="6">Loading sessions...</td></tr>
24194
- </tbody>
24195
- </table>
24196
- </section>
24197
-
24198
- <section class="panel">
24199
- <header>Session Replay</header>
24200
- <div id="replay-meta" class="status">Select a session to inspect timeline events.</div>
24201
- <table>
24202
- <thead>
24203
- <tr>
24204
- <th>Timestamp</th>
24205
- <th>Type</th>
24206
- <th>Status</th>
24207
- <th>Cost</th>
24208
- <th>Prompt</th>
24209
- </tr>
24210
- </thead>
24211
- <tbody id="replay-body">
24212
- <tr><td colspan="5">No session selected.</td></tr>
24213
- </tbody>
24214
- </table>
24215
- </section>
24334
+ function parseTextSegments(text) {
24335
+ var segs = [], re = /\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, last = 0, m;
24336
+ while ((m = re.exec(text)) !== null) {
24337
+ if (m.index > last) segs.push({type:'text',content:text.slice(last,m.index)});
24338
+ segs.push({type:'code',lang:m[1]||'text',content:m[2]||''});
24339
+ last = m.index + m[0].length;
24340
+ }
24341
+ if (last < text.length) segs.push({type:'text',content:text.slice(last)});
24342
+ return segs.length > 0 ? segs : [{type:'text',content:text}];
24343
+ }
24216
24344
 
24217
- <section id="status" class="status">Fetching data from local API bridge...</section>
24218
- </main>
24219
- <script>
24220
- const sessionsBody = document.getElementById("sessions-body");
24221
- const status = document.getElementById("status");
24222
- const sessionsMetric = document.getElementById("metric-sessions");
24223
- const costMetric = document.getElementById("metric-cost");
24224
- const latestMetric = document.getElementById("metric-latest");
24225
- const replayMeta = document.getElementById("replay-meta");
24226
- const replayBody = document.getElementById("replay-body");
24227
- let selectedSessionId = null;
24345
+ function renderFormattedText(text) {
24346
+ var segs = parseTextSegments(text);
24347
+ return segs.map(function(s){
24348
+ if(s.type==='code') return '<div class="cblock"><div class="cblock-hd">' + esc(s.lang) + '</div><pre><code class="language-' + esc(s.lang) + '">' + highlight(s.content,s.lang) + '</code></pre></div>';
24349
+ return '<span>' + esc(s.content) + '</span>';
24350
+ }).join('');
24351
+ }
24228
24352
 
24229
- function formatMoney(value) {
24230
- return "$" + value.toFixed(2);
24231
- }
24353
+ function renderPromptCard(g, idx) {
24354
+ var stats = '';
24355
+ if(g.commits.length>0) stats += '<span class="badge commit">' + (g.commits.length===1?esc(g.commits[0].sha.slice(0,7)):g.commits.length+' commits') + '</span>';
24356
+ if(g.totalToolCalls>0) stats += '<span class="badge purple">' + g.totalToolCalls + ' tools</span>';
24357
+ if(g.filesWritten.length>0) stats += '<span class="badge green">' + g.filesWritten.length + ' written</span>';
24358
+ if(g.filesRead.length>0) stats += '<span class="badge">' + g.filesRead.length + ' read</span>';
24359
+ if(g.totalCostUsd>0) stats += '<span class="badge orange">' + fmt$4(g.totalCostUsd) + '</span>';
24232
24360
 
24233
- function formatDate(value) {
24234
- try {
24235
- return new Date(value).toLocaleString();
24236
- } catch {
24237
- return value;
24238
- }
24239
- }
24361
+ var body = '';
24362
+ if(g.commits.length>0){
24363
+ body += '<div class="pcommits">';
24364
+ g.commits.forEach(function(c){body += '<div class="pcommit"><span class="commit-sha">' + esc(c.sha.slice(0,7)) + '</span><span class="commit-msg">' + esc(c.message||'no message') + '</span></div>';});
24365
+ body += '</div>';
24366
+ }
24367
+ g.toolEvents.forEach(function(ev){body += renderEventRow(ev);});
24368
+ if(g.filesWritten.length>0||g.filesRead.length>0){
24369
+ body += '<div class="fsummary">';
24370
+ if(g.filesWritten.length>0){body += '<div class="fsg"><span class="fsg-label written">written</span>';g.filesWritten.forEach(function(f){body += '<span class="fsg-path">' + esc(f) + '</span>';});body += '</div>';}
24371
+ if(g.filesRead.length>0){body += '<div class="fsg"><span class="fsg-label read">read</span>';g.filesRead.forEach(function(f){body += '<span class="fsg-path">' + esc(f) + '</span>';});body += '</div>';}
24372
+ body += '</div>';
24373
+ }
24374
+ if(g.responseText) body += '<div class="rblock"><div class="rblock-label">Response</div><div class="rblock-text">' + renderFormattedText(g.responseText) + '</div></div>';
24240
24375
 
24241
- function escapeHtml(value) {
24242
- return String(value)
24243
- .replaceAll("&", "&amp;")
24244
- .replaceAll("<", "&lt;")
24245
- .replaceAll(">", "&gt;")
24246
- .replaceAll(""", "&quot;")
24247
- .replaceAll("'", "&#39;");
24248
- }
24249
-
24250
- function setReplayPlaceholder(message) {
24251
- replayBody.innerHTML = "<tr><td colspan=\\"5\\">" + escapeHtml(message) + "</td></tr>";
24252
- }
24376
+ return '<div class="pg" id="pg-' + esc(g.promptId.slice(0,12)) + '">' +
24377
+ '<div class="pg-hd" onclick="togglePrompt(this)">' +
24378
+ '<div class="pg-idx">' + idx + '</div>' +
24379
+ '<div class="pg-txt trunc">' + esc(g.promptText || 'prompt ' + g.promptId.slice(0,8)) + '</div>' +
24380
+ '<div class="pg-stats">' + stats + '</div>' +
24381
+ '<div class="pg-arrow">&gt;</div>' +
24382
+ '</div>' +
24383
+ '<div class="pg-body" style="display:none">' + body + '</div>' +
24384
+ '</div>';
24385
+ }
24253
24386
 
24254
- function renderSessionReplay(session) {
24255
- if (typeof session !== "object" || session === null) {
24256
- replayMeta.classList.add("error");
24257
- replayMeta.textContent = "Replay payload is invalid.";
24258
- setReplayPlaceholder("Replay payload is invalid.");
24259
- return;
24260
- }
24387
+ window.togglePrompt = function(hd) {
24388
+ var pg = hd.parentElement;
24389
+ var body = pg.querySelector('.pg-body');
24390
+ var txt = pg.querySelector('.pg-txt');
24391
+ var arrow = pg.querySelector('.pg-arrow');
24392
+ var open = body.style.display !== 'none';
24393
+ body.style.display = open ? 'none' : '';
24394
+ pg.classList.toggle('expanded', !open);
24395
+ txt.classList.toggle('trunc', open);
24396
+ arrow.classList.toggle('open', !open);
24397
+ };
24261
24398
 
24262
- const timeline = Array.isArray(session.timeline) ? session.timeline : [];
24263
- const promptCount = typeof session.metrics?.promptCount === "number" ? session.metrics.promptCount : 0;
24264
- const toolCallCount = typeof session.metrics?.toolCallCount === "number" ? session.metrics.toolCallCount : 0;
24265
- const totalCostUsd = typeof session.metrics?.totalCostUsd === "number" ? session.metrics.totalCostUsd : 0;
24266
- replayMeta.classList.remove("error");
24267
- replayMeta.textContent = "Session " + session.sessionId + " | prompts " + promptCount
24268
- + " | tools " + toolCallCount + " | cost " + formatMoney(totalCostUsd);
24399
+ function renderSessions() {
24400
+ var area = document.getElementById('sessions-area');
24401
+ if (sessions.length === 0) { area.innerHTML = '<div class="empty">No sessions captured yet.</div>'; return; }
24402
+ var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
24403
+ sessions.forEach(function(s){
24404
+ var active = s.sessionId === selectedId ? ' active' : '';
24405
+ var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
24406
+ var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
24407
+ var lines = (s.linesAdded > 0 || s.linesRemoved > 0) ? '<span class="ls green">+' + s.linesAdded + '</span><span class="ls red">-' + s.linesRemoved + '</span>' : '<span style="color:var(--text-dim)">-</span>';
24408
+ h += '<tr class="srow' + active + '" data-sid="' + esc(s.sessionId) + '" onclick="selectSession(this.dataset.sid)"><td>' + esc(s.sessionId.slice(0,10)) + '</td><td class="repo-cell">' + esc(repo) + '</td><td>' + fmtDate(s.startedAt) + '</td><td>' + s.promptCount + '</td><td>' + fmt$(s.totalCostUsd) + '</td><td>' + commits + '</td><td>' + lines + '</td></tr>';
24409
+ });
24410
+ h += '</tbody></table>';
24411
+ area.innerHTML = h;
24412
+ }
24269
24413
 
24270
- if (timeline.length === 0) {
24271
- setReplayPlaceholder("No timeline events for this session.");
24272
- return;
24273
- }
24414
+ function renderMetrics() {
24415
+ document.getElementById('m-sessions').textContent = sessions.length;
24416
+ document.getElementById('m-cost').textContent = fmt$(sessions.reduce(function(s,x){return s+x.totalCostUsd;},0));
24417
+ document.getElementById('m-prompts').textContent = sessions.reduce(function(s,x){return s+x.promptCount;},0);
24418
+ document.getElementById('m-tools').textContent = sessions.reduce(function(s,x){return s+x.toolCallCount;},0);
24419
+ var tc = sessions.reduce(function(s,x){return s+x.commitCount;},0);
24420
+ var sc = sessions.filter(function(x){return x.commitCount>0;}).length;
24421
+ document.getElementById('m-commits').textContent = tc;
24422
+ document.getElementById('m-commits-det').textContent = sc + '/' + sessions.length + ' sessions produced commits';
24423
+ }
24274
24424
 
24275
- const rows = timeline.map((event) => {
24276
- const timestamp = typeof event.timestamp === "string" ? formatDate(event.timestamp) : "-";
24277
- const type = typeof event.type === "string" ? event.type : "-";
24278
- const eventStatus = typeof event.status === "string" ? event.status : "-";
24279
- const cost = typeof event.costUsd === "number" ? formatMoney(event.costUsd) : "-";
24280
- const prompt = typeof event.promptId === "string" ? event.promptId : "-";
24281
- return "<tr>"
24282
- + "<td>" + escapeHtml(timestamp) + "</td>"
24283
- + "<td>" + escapeHtml(type) + "</td>"
24284
- + "<td>" + escapeHtml(eventStatus) + "</td>"
24285
- + "<td>" + escapeHtml(cost) + "</td>"
24286
- + "<td>" + escapeHtml(prompt) + "</td>"
24287
- + "</tr>";
24288
- }).join("");
24289
- replayBody.innerHTML = rows;
24290
- }
24425
+ function renderCostChart() {
24426
+ var el = document.getElementById('cost-chart');
24427
+ if (costPoints.length === 0) { el.innerHTML = '<div class="empty">No cost data yet.</div>'; return; }
24428
+ var pts = costPoints.slice(-7);
24429
+ var max = Math.max(0.01, Math.max.apply(null, pts.map(function(p){return p.totalCostUsd;})));
24430
+ var h = '<div class="chart">';
24431
+ pts.forEach(function(p){
24432
+ var ht = Math.max(4, Math.round((p.totalCostUsd / max) * 140));
24433
+ h += '<div class="chart-col"><div class="chart-bar" style="height:' + ht + 'px"></div><div class="chart-value">' + fmt$(p.totalCostUsd) + '</div><div class="chart-label">' + esc(p.date.slice(5)) + '</div></div>';
24434
+ });
24435
+ h += '</div>';
24436
+ el.innerHTML = h;
24437
+ }
24291
24438
 
24292
- async function loadSessionReplay(sessionId) {
24293
- selectedSessionId = sessionId;
24294
- try {
24295
- const response = await fetch("/api/session/" + encodeURIComponent(sessionId));
24296
- if (response.status === 404) {
24297
- replayMeta.classList.add("error");
24298
- replayMeta.textContent = "Session replay not found.";
24299
- setReplayPlaceholder("Session replay not found.");
24300
- return;
24301
- }
24302
- if (!response.ok) {
24303
- throw new Error("session replay bridge failed with status " + response.status);
24304
- }
24305
- const payload = await response.json();
24306
- if (payload?.status !== "ok" || typeof payload.session !== "object") {
24307
- throw new Error("unexpected replay payload format");
24308
- }
24309
- renderSessionReplay(payload.session);
24310
- } catch (error) {
24311
- replayMeta.classList.add("error");
24312
- replayMeta.textContent = String(error);
24313
- setReplayPlaceholder("Failed to load replay.");
24314
- }
24315
- }
24439
+ function renderReplay() {
24440
+ var area = document.getElementById('replay-area');
24441
+ var label = document.getElementById('replay-label');
24442
+ if (!replay) {
24443
+ label.textContent = selectedId ? selectedId.slice(0,12) : 'select a session';
24444
+ area.innerHTML = '<div class="empty">No replay data.</div>';
24445
+ return;
24446
+ }
24447
+ label.textContent = replay.sessionId.slice(0,12) + ' \\u2014 ' + replay.metrics.promptCount + ' prompts, ' + fmt$(replay.metrics.totalCostUsd);
24448
+ var h = '';
24449
+ // meta
24450
+ h += '<div class="tm">';
24451
+ h += '<span class="tmi">Cost <span class="badge orange">' + fmt$4(replay.metrics.totalCostUsd) + '</span></span>';
24452
+ h += '<span class="tmi">Tokens <span class="badge cyan">' + replay.metrics.totalInputTokens + ' in / ' + replay.metrics.totalOutputTokens + ' out</span></span>';
24453
+ if (replay.metrics.linesAdded > 0 || replay.metrics.linesRemoved > 0) h += '<span class="tmi">Lines <span class="badge green">+' + replay.metrics.linesAdded + '</span> <span class="badge red">-' + replay.metrics.linesRemoved + '</span></span>';
24454
+ if (replay.metrics.modelsUsed.length > 0) h += '<span class="tmi">' + esc(replay.metrics.modelsUsed.join(', ')) + '</span>';
24455
+ if (replay.metrics.filesTouched.length > 0) h += '<span class="tmi">' + replay.metrics.filesTouched.length + ' files</span>';
24456
+ h += '</div>';
24457
+ // outcome
24458
+ if (replay.commits.length > 0 || replay.pullRequests.length > 0 || replay.gitBranch) {
24459
+ h += '<div class="outcome"><div class="outcome-hd">Outcome</div><div class="outcome-row">';
24460
+ if (replay.gitBranch) h += '<span class="outcome-item"><span class="outcome-lbl">branch</span><span class="outcome-val">' + esc(replay.gitBranch) + '</span></span>';
24461
+ if (replay.commits.length > 0) h += '<span class="outcome-item"><span class="outcome-lbl">' + (replay.commits.length===1?'commit':'commits') + '</span><span class="outcome-val">' + replay.commits.length + '</span></span>';
24462
+ if (replay.metrics.linesAdded > 0 || replay.metrics.linesRemoved > 0) h += '<span class="outcome-item"><span class="outcome-lbl">lines</span><span class="outcome-val"><span class="ls green">+' + replay.metrics.linesAdded + '</span><span class="ls red">-' + replay.metrics.linesRemoved + '</span></span></span>';
24463
+ if (replay.metrics.filesTouched.length > 0) h += '<span class="outcome-item"><span class="outcome-lbl">files</span><span class="outcome-val">' + replay.metrics.filesTouched.length + '</span></span>';
24464
+ h += '</div>';
24465
+ if (replay.commits.length > 0) {
24466
+ h += '<div class="outcome-commits">';
24467
+ replay.commits.forEach(function(c){ h += '<div class="outcome-cr"><span class="commit-sha">' + esc(c.sha.slice(0,7)) + '</span><span class="commit-msg">' + esc(c.message||'-') + '</span>' + (c.promptId ? '<span class="commit-pl">prompt ' + esc(c.promptId.slice(0,6)) + '</span>' : '') + '</div>'; });
24468
+ h += '</div>';
24469
+ }
24470
+ if (replay.pullRequests.length > 0) {
24471
+ h += '<div class="outcome-prs">';
24472
+ replay.pullRequests.forEach(function(pr){ h += '<div class="outcome-pr"><span class="pr-badge">' + esc(pr.state) + '</span><span class="pr-label">PR #' + pr.prNumber + '</span><span class="pr-repo">' + esc(pr.repo) + '</span>' + (pr.url ? '<a class="pr-link" href="' + esc(pr.url) + '" target="_blank" rel="noopener noreferrer">' + esc(pr.url) + '</a>' : '') + '</div>'; });
24473
+ h += '</div>';
24474
+ }
24475
+ h += '</div>';
24476
+ }
24477
+ // prompt groups
24478
+ var groups = buildPromptGroups(replay.timeline, replay.commits);
24479
+ if (groups.length === 0) {
24480
+ h += '<div class="empty">No prompts in this session.</div>';
24481
+ } else {
24482
+ groups.forEach(function(g, i) { h += renderPromptCard(g, i + 1); });
24483
+ }
24484
+ area.innerHTML = h;
24485
+ }
24316
24486
 
24317
- function bindReplayButtons() {
24318
- const buttons = sessionsBody.querySelectorAll(".replay-button");
24319
- buttons.forEach((button) => {
24320
- button.addEventListener("click", () => {
24321
- const sessionId = button.getAttribute("data-session-id");
24322
- if (sessionId === null || sessionId.length === 0) {
24323
- return;
24324
- }
24325
- void loadSessionReplay(sessionId);
24326
- });
24327
- });
24328
- }
24487
+ function parseSummary(v) {
24488
+ var r = asRec(v); if (!r) return null;
24489
+ var sid = readStr(r,'sessionId'), uid = readStr(r,'userId'), sa = readStr(r,'startedAt');
24490
+ if (!sid || !uid || !sa) return null;
24491
+ return {sessionId:sid,userId:uid,gitRepo:typeof r.gitRepo==='string'?r.gitRepo:null,gitBranch:typeof r.gitBranch==='string'?r.gitBranch:null,startedAt:sa,endedAt:typeof r.endedAt==='string'?r.endedAt:null,promptCount:readNum(r,'promptCount')||0,toolCallCount:readNum(r,'toolCallCount')||0,totalCostUsd:readNum(r,'totalCostUsd')||0,commitCount:readNum(r,'commitCount')||0,linesAdded:readNum(r,'linesAdded')||0,linesRemoved:readNum(r,'linesRemoved')||0};
24492
+ }
24329
24493
 
24330
- function renderSessions(sessions) {
24331
- if (!Array.isArray(sessions) || sessions.length === 0) {
24332
- sessionsBody.innerHTML = "<tr><td colspan=\\"6\\">No sessions yet.</td></tr>";
24333
- sessionsMetric.textContent = "0";
24334
- costMetric.textContent = "$0.00";
24335
- latestMetric.textContent = "-";
24336
- replayMeta.classList.remove("error");
24337
- replayMeta.textContent = "Select a session to inspect timeline events.";
24338
- setReplayPlaceholder("No session selected.");
24339
- return;
24340
- }
24494
+ function parseCostPoint(v) {
24495
+ var r = asRec(v); if (!r) return null;
24496
+ var d = readStr(r,'date'); if (!d) return null;
24497
+ return {date:d,totalCostUsd:readNum(r,'totalCostUsd')||0,sessionCount:readNum(r,'sessionCount')||0,promptCount:readNum(r,'promptCount')||0,toolCallCount:readNum(r,'toolCallCount')||0};
24498
+ }
24341
24499
 
24342
- const rows = sessions.map((session) => {
24343
- const repo = session.gitRepo ?? "-";
24344
- const cost = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
24345
- return "<tr>"
24346
- + "<td>" + escapeHtml(session.sessionId) + "</td>"
24347
- + "<td>" + escapeHtml(session.userId) + "</td>"
24348
- + "<td>" + escapeHtml(repo) + "</td>"
24349
- + "<td>" + escapeHtml(formatDate(session.startedAt)) + "</td>"
24350
- + "<td>" + escapeHtml(formatMoney(cost)) + "</td>"
24351
- + "<td><button type=\\"button\\" class=\\"replay-button\\" data-session-id=\\""
24352
- + escapeHtml(session.sessionId)
24353
- + "\\">View</button></td>"
24354
- + "</tr>";
24355
- }).join("");
24356
- sessionsBody.innerHTML = rows;
24357
- bindReplayButtons();
24500
+ function sortLatest(arr) { return arr.slice().sort(function(a,b){ return Date.parse(ensureUtc(b.startedAt)) - Date.parse(ensureUtc(a.startedAt)); }); }
24358
24501
 
24359
- const totalCost = sessions.reduce((sum, session) => {
24360
- const value = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
24361
- return sum + value;
24362
- }, 0);
24363
- const latest = sessions
24364
- .map((session) => session.startedAt)
24365
- .filter((value) => typeof value === "string")
24366
- .sort()
24367
- .at(-1) ?? "-";
24502
+ function setSessions(raw) {
24503
+ sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
24504
+ renderMetrics();
24505
+ renderSessions();
24506
+ if (sessions.length > 0 && (!selectedId || !sessions.some(function(s){return s.sessionId===selectedId;}))) {
24507
+ selectSession(sessions[0].sessionId);
24508
+ }
24509
+ }
24368
24510
 
24369
- sessionsMetric.textContent = String(sessions.length);
24370
- costMetric.textContent = formatMoney(totalCost);
24371
- latestMetric.textContent = latest === "-" ? "-" : formatDate(latest);
24511
+ window.selectSession = function(sid) {
24512
+ selectedId = sid;
24513
+ renderSessions();
24514
+ loadReplay(sid);
24515
+ };
24372
24516
 
24373
- const selectedInList = selectedSessionId !== null && sessions.some((session) => session.sessionId === selectedSessionId);
24374
- if (!selectedInList && sessions[0]?.sessionId !== undefined) {
24375
- void loadSessionReplay(sessions[0].sessionId);
24376
- }
24377
- }
24517
+ function loadReplay(sid) {
24518
+ replay = null;
24519
+ renderReplay();
24520
+ fetch('/api/session/' + encodeURIComponent(sid), {cache:'no-store'}).then(function(r){
24521
+ if (r.status === 404) return null;
24522
+ if (!r.ok) throw new Error('replay failed (' + r.status + ')');
24523
+ return r.json();
24524
+ }).then(function(payload){
24525
+ if (!payload) return;
24526
+ var p = asRec(payload);
24527
+ if (!p || readStr(p,'status') !== 'ok') return;
24528
+ replay = parseReplay(p.session);
24529
+ renderReplay();
24530
+ }).catch(function(e){ document.getElementById('replay-area').innerHTML = '<div class="empty" style="color:var(--red)">' + esc(String(e)) + '</div>'; });
24531
+ }
24378
24532
 
24379
- async function loadSessions() {
24380
- try {
24381
- const response = await fetch("/api/sessions");
24382
- if (!response.ok) {
24383
- throw new Error("dashboard bridge failed with status " + response.status);
24384
- }
24385
- const payload = await response.json();
24386
- if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
24387
- throw new Error("unexpected payload format");
24388
- }
24389
- renderSessions(payload.sessions);
24390
- status.classList.remove("error");
24391
- status.textContent = "Data source connected.";
24392
- } catch (error) {
24393
- sessionsBody.innerHTML = "<tr><td colspan=\\"6\\">Failed to load sessions.</td></tr>";
24394
- status.classList.add("error");
24395
- status.textContent = String(error);
24396
- }
24397
- }
24533
+ function loadSnapshot() {
24534
+ return Promise.all([
24535
+ fetch('/api/sessions', {cache:'no-store'}),
24536
+ fetch('/api/analytics/cost/daily', {cache:'no-store'}).catch(function(){return null;})
24537
+ ]).then(function(results){
24538
+ var sr = results[0], cr = results[1];
24539
+ if (!sr.ok) throw new Error('sessions failed (' + sr.status + ')');
24540
+ return Promise.all([sr.json(), cr && cr.ok ? cr.json() : {points:[]}]);
24541
+ }).then(function(data){
24542
+ var sp = asRec(data[0]), cp = asRec(data[1]);
24543
+ if (sp && Array.isArray(sp.sessions)) setSessions(sp.sessions);
24544
+ if (cp && Array.isArray(cp.points)) { costPoints = cp.points.map(parseCostPoint).filter(Boolean); renderCostChart(); }
24545
+ document.getElementById('status').className = 'status-banner';
24546
+ }).catch(function(e){
24547
+ document.getElementById('status').className = 'status-banner warning';
24548
+ document.getElementById('status').textContent = String(e);
24549
+ });
24550
+ }
24398
24551
 
24399
- function startLiveSessionsStream() {
24400
- if (typeof EventSource === "undefined") {
24401
- status.classList.remove("error");
24402
- status.textContent = "Live stream unavailable. Snapshot mode enabled.";
24403
- return;
24552
+ function boot() {
24553
+ loadSnapshot().then(function(){
24554
+ if (typeof EventSource !== 'undefined') {
24555
+ var es = new EventSource('/api/sessions/stream');
24556
+ es.addEventListener('sessions', function(event) {
24557
+ var p = asRec(JSON.parse(event.data));
24558
+ if (p && Array.isArray(p.sessions)) {
24559
+ setSessions(p.sessions);
24560
+ document.getElementById('stream-label').textContent = 'live';
24561
+ document.getElementById('status').className = 'status-banner';
24562
+ document.getElementById('status').textContent = 'Live';
24404
24563
  }
24564
+ });
24565
+ es.addEventListener('bridge_error', function(event) {
24566
+ document.getElementById('status').className = 'status-banner warning';
24567
+ document.getElementById('status').textContent = 'Bridge error: ' + event.data;
24568
+ });
24569
+ es.onerror = function() {
24570
+ document.getElementById('stream-label').textContent = 'polling';
24571
+ document.getElementById('status').textContent = 'Polling';
24572
+ };
24573
+ } else {
24574
+ document.getElementById('stream-label').textContent = 'polling';
24575
+ }
24576
+ setInterval(function(){ loadSnapshot(); }, 15000);
24577
+ });
24578
+ }
24405
24579
 
24406
- status.classList.remove("error");
24407
- status.textContent = "Connecting to live session stream...";
24408
-
24409
- const stream = new EventSource("/api/sessions/stream");
24410
- stream.addEventListener("sessions", (event) => {
24411
- try {
24412
- const payload = JSON.parse(event.data);
24413
- if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
24414
- throw new Error("unexpected stream payload");
24415
- }
24416
- renderSessions(payload.sessions);
24417
- status.classList.remove("error");
24418
- status.textContent = "Live session stream connected.";
24419
- } catch (error) {
24420
- status.classList.add("error");
24421
- status.textContent = String(error);
24422
- }
24423
- });
24424
-
24425
- stream.addEventListener("bridge_error", (event) => {
24426
- status.classList.add("error");
24427
- status.textContent = "Bridge error: " + event.data;
24428
- });
24429
-
24430
- stream.onerror = () => {
24431
- status.classList.add("error");
24432
- status.textContent = "Live stream disconnected. Retrying...";
24433
- };
24434
- }
24435
-
24436
- async function boot() {
24437
- await loadSessions();
24438
- startLiveSessionsStream();
24439
- }
24440
-
24441
- void boot();
24442
- </script>
24443
- </body>
24580
+ boot();
24581
+ })();
24582
+ </script>
24583
+ </body>
24444
24584
  </html>`;
24445
24585
  }
24446
24586
 
@@ -24770,6 +24910,19 @@ async function startDashboardServer(options = {}) {
24770
24910
  startSessionsSseBridge(req, res, sessionsProvider);
24771
24911
  return;
24772
24912
  }
24913
+ if (pathname === "/api/analytics/cost/daily") {
24914
+ void fetch(`${apiBaseUrl}/v1/analytics/cost/daily`).then(async (apiResponse) => {
24915
+ if (!apiResponse.ok) {
24916
+ sendJson(res, 502, { status: "error", message: `api returned status ${String(apiResponse.status)}` });
24917
+ return;
24918
+ }
24919
+ const payload = await apiResponse.json();
24920
+ sendJson(res, 200, payload);
24921
+ }).catch((error) => {
24922
+ sendJson(res, 502, { status: "error", message: `failed to fetch daily cost: ${String(error)}` });
24923
+ });
24924
+ return;
24925
+ }
24773
24926
  if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
24774
24927
  const encodedSessionId = segments[2];
24775
24928
  let sessionId = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {