@codemieai/code 0.3.1 → 0.4.0

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 (120) hide show
  1. package/README.md +2 -0
  2. package/dist/agents/core/session/BaseSessionAdapter.d.ts +5 -2
  3. package/dist/agents/core/session/BaseSessionAdapter.d.ts.map +1 -1
  4. package/dist/agents/core/types.d.ts +11 -0
  5. package/dist/agents/core/types.d.ts.map +1 -1
  6. package/dist/agents/plugins/claude/claude.plugin.js +3 -3
  7. package/dist/agents/plugins/claude/claude.session.d.ts +13 -0
  8. package/dist/agents/plugins/claude/claude.session.d.ts.map +1 -1
  9. package/dist/agents/plugins/claude/claude.session.js +122 -42
  10. package/dist/agents/plugins/claude/claude.session.js.map +1 -1
  11. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js +12 -8
  12. package/dist/agents/plugins/claude/plugin/statusline.mjs +11 -3
  13. package/dist/agents/plugins/claude/session/claude-file-operation.d.ts +38 -0
  14. package/dist/agents/plugins/claude/session/claude-file-operation.d.ts.map +1 -0
  15. package/dist/agents/plugins/claude/session/claude-file-operation.js +67 -0
  16. package/dist/agents/plugins/claude/session/claude-file-operation.js.map +1 -0
  17. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts +0 -4
  18. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts.map +1 -1
  19. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js +7 -56
  20. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js.map +1 -1
  21. package/dist/bin/proxy-daemon.js +54 -5
  22. package/dist/bin/proxy-daemon.js.map +1 -1
  23. package/dist/cli/commands/analytics/aggregator.d.ts +1 -1
  24. package/dist/cli/commands/analytics/aggregator.d.ts.map +1 -1
  25. package/dist/cli/commands/analytics/aggregator.js +84 -23
  26. package/dist/cli/commands/analytics/aggregator.js.map +1 -1
  27. package/dist/cli/commands/analytics/cost/cost-calculator.d.ts +10 -0
  28. package/dist/cli/commands/analytics/cost/cost-calculator.d.ts.map +1 -0
  29. package/dist/cli/commands/analytics/cost/cost-calculator.js +24 -0
  30. package/dist/cli/commands/analytics/cost/cost-calculator.js.map +1 -0
  31. package/dist/cli/commands/analytics/cost/cost-enricher.d.ts +24 -0
  32. package/dist/cli/commands/analytics/cost/cost-enricher.d.ts.map +1 -0
  33. package/dist/cli/commands/analytics/cost/cost-enricher.js +152 -0
  34. package/dist/cli/commands/analytics/cost/cost-enricher.js.map +1 -0
  35. package/dist/cli/commands/analytics/cost/pricing.d.ts +23 -0
  36. package/dist/cli/commands/analytics/cost/pricing.d.ts.map +1 -0
  37. package/dist/cli/commands/analytics/cost/pricing.js +82 -0
  38. package/dist/cli/commands/analytics/cost/pricing.js.map +1 -0
  39. package/dist/cli/commands/analytics/cost/pricing.json +975 -0
  40. package/dist/cli/commands/analytics/cost/types.d.ts +51 -0
  41. package/dist/cli/commands/analytics/cost/types.d.ts.map +1 -0
  42. package/dist/cli/commands/analytics/cost/types.js +8 -0
  43. package/dist/cli/commands/analytics/cost/types.js.map +1 -0
  44. package/dist/cli/commands/analytics/cost/usage-readers.d.ts +40 -0
  45. package/dist/cli/commands/analytics/cost/usage-readers.d.ts.map +1 -0
  46. package/dist/cli/commands/analytics/cost/usage-readers.js +165 -0
  47. package/dist/cli/commands/analytics/cost/usage-readers.js.map +1 -0
  48. package/dist/cli/commands/analytics/data-loader.d.ts +11 -0
  49. package/dist/cli/commands/analytics/data-loader.d.ts.map +1 -1
  50. package/dist/cli/commands/analytics/data-loader.js +7 -0
  51. package/dist/cli/commands/analytics/data-loader.js.map +1 -1
  52. package/dist/cli/commands/analytics/index.d.ts.map +1 -1
  53. package/dist/cli/commands/analytics/index.js +92 -10
  54. package/dist/cli/commands/analytics/index.js.map +1 -1
  55. package/dist/cli/commands/analytics/native-loader.d.ts +48 -0
  56. package/dist/cli/commands/analytics/native-loader.d.ts.map +1 -0
  57. package/dist/cli/commands/analytics/native-loader.js +220 -0
  58. package/dist/cli/commands/analytics/native-loader.js.map +1 -0
  59. package/dist/cli/commands/analytics/report/assets/chart.umd.js +14 -0
  60. package/dist/cli/commands/analytics/report/assets/codemie-bundle.css +1 -0
  61. package/dist/cli/commands/analytics/report/client/app.js +676 -0
  62. package/dist/cli/commands/analytics/report/payload-builder.d.ts +15 -0
  63. package/dist/cli/commands/analytics/report/payload-builder.d.ts.map +1 -0
  64. package/dist/cli/commands/analytics/report/payload-builder.js +107 -0
  65. package/dist/cli/commands/analytics/report/payload-builder.js.map +1 -0
  66. package/dist/cli/commands/analytics/report/report-generator.d.ts +26 -0
  67. package/dist/cli/commands/analytics/report/report-generator.d.ts.map +1 -0
  68. package/dist/cli/commands/analytics/report/report-generator.js +58 -0
  69. package/dist/cli/commands/analytics/report/report-generator.js.map +1 -0
  70. package/dist/cli/commands/analytics/report/template.html +217 -0
  71. package/dist/cli/commands/analytics/report/types.d.ts +55 -0
  72. package/dist/cli/commands/analytics/report/types.d.ts.map +1 -0
  73. package/dist/cli/commands/analytics/report/types.js +6 -0
  74. package/dist/cli/commands/analytics/report/types.js.map +1 -0
  75. package/dist/cli/commands/analytics/types.d.ts +13 -0
  76. package/dist/cli/commands/analytics/types.d.ts.map +1 -1
  77. package/dist/cli/commands/proxy/daemon-manager.d.ts +5 -0
  78. package/dist/cli/commands/proxy/daemon-manager.d.ts.map +1 -1
  79. package/dist/cli/commands/proxy/daemon-manager.js +25 -9
  80. package/dist/cli/commands/proxy/daemon-manager.js.map +1 -1
  81. package/dist/cli/commands/proxy/health-check.d.ts +16 -0
  82. package/dist/cli/commands/proxy/health-check.d.ts.map +1 -0
  83. package/dist/cli/commands/proxy/health-check.js +81 -0
  84. package/dist/cli/commands/proxy/health-check.js.map +1 -0
  85. package/dist/cli/commands/proxy/index.d.ts.map +1 -1
  86. package/dist/cli/commands/proxy/index.js +54 -4
  87. package/dist/cli/commands/proxy/index.js.map +1 -1
  88. package/dist/cli/commands/proxy/watcher.d.ts +31 -0
  89. package/dist/cli/commands/proxy/watcher.d.ts.map +1 -0
  90. package/dist/cli/commands/proxy/watcher.js +97 -0
  91. package/dist/cli/commands/proxy/watcher.js.map +1 -0
  92. package/dist/cli/commands/skills/setup/sync-plugin.d.ts +15 -0
  93. package/dist/cli/commands/skills/setup/sync-plugin.d.ts.map +1 -0
  94. package/dist/cli/commands/skills/setup/sync-plugin.js +63 -0
  95. package/dist/cli/commands/skills/setup/sync-plugin.js.map +1 -0
  96. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +0 -1
  97. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
  98. package/dist/providers/plugins/sso/proxy/plugins/index.js +0 -3
  99. package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
  100. package/dist/providers/plugins/sso/proxy/proxy-errors.js +2 -2
  101. package/dist/providers/plugins/sso/proxy/proxy-errors.js.map +1 -1
  102. package/dist/providers/plugins/sso/proxy/proxy-types.d.ts +6 -0
  103. package/dist/providers/plugins/sso/proxy/proxy-types.d.ts.map +1 -1
  104. package/dist/providers/plugins/sso/proxy/sso.proxy.d.ts +1 -10
  105. package/dist/providers/plugins/sso/proxy/sso.proxy.d.ts.map +1 -1
  106. package/dist/providers/plugins/sso/proxy/sso.proxy.js +39 -76
  107. package/dist/providers/plugins/sso/proxy/sso.proxy.js.map +1 -1
  108. package/dist/providers/plugins/sso/session/processors/metrics/metrics-aggregator.js +14 -4
  109. package/dist/providers/plugins/sso/session/processors/metrics/metrics-aggregator.js.map +1 -1
  110. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.d.ts.map +1 -1
  111. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.js +5 -0
  112. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.js.map +1 -1
  113. package/dist/providers/plugins/sso/session/processors/metrics/metrics-types.d.ts +2 -0
  114. package/dist/providers/plugins/sso/session/processors/metrics/metrics-types.d.ts.map +1 -1
  115. package/package.json +2 -3
  116. package/scripts/copy-plugins.js +39 -0
  117. package/dist/providers/plugins/sso/proxy/plugins/session-expiry-handler.plugin.d.ts +0 -9
  118. package/dist/providers/plugins/sso/proxy/plugins/session-expiry-handler.plugin.d.ts.map +0 -1
  119. package/dist/providers/plugins/sso/proxy/plugins/session-expiry-handler.plugin.js +0 -23
  120. package/dist/providers/plugins/sso/proxy/plugins/session-expiry-handler.plugin.js.map +0 -1
@@ -0,0 +1,676 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * CodeMie Analytics — client app (vanilla JS, no build).
4
+ * Reads window.__ANALYTICS__ (ReportPayload) and renders 7 client-side views.
5
+ * All filtering/aggregation happens here so the report needs no server.
6
+ */
7
+ (function () {
8
+ 'use strict';
9
+
10
+ var DATA = window.__ANALYTICS__;
11
+ var root = document.getElementById('view-root');
12
+ if (!DATA || !root) {
13
+ if (root) root.innerHTML = '<div class="empty">No analytics data embedded in this report.</div>';
14
+ return;
15
+ }
16
+
17
+ // ---- palette ------------------------------------------------------------
18
+ var PALETTE = ['#7C5CFC', '#2297F6', '#F5A534', '#06B6D4', '#259F4C', '#F9303C', '#C084FC', '#E879A6'];
19
+ var AGENT_COLORS = { claude: '#7C5CFC', 'claude-acp': '#9D7BFF', 'claude-desktop': '#B79DFF', gemini: '#F5A534', codex: '#06B6D4', opencode: '#259F4C', 'codemie-code': '#2297F6' };
20
+ var seenAgentColor = {};
21
+ var colorCursor = 0;
22
+ function colorFor(agent) {
23
+ if (AGENT_COLORS[agent]) return AGENT_COLORS[agent];
24
+ if (!seenAgentColor[agent]) { seenAgentColor[agent] = PALETTE[colorCursor % PALETTE.length]; colorCursor++; }
25
+ return seenAgentColor[agent];
26
+ }
27
+
28
+ // ---- formatting ---------------------------------------------------------
29
+ function fmtNum(n) { return (n || 0).toLocaleString('en-US'); }
30
+ function fmtUSD(n) {
31
+ if (!n) return '$0.00';
32
+ if (n < 0.01) return '$' + n.toFixed(4);
33
+ if (n < 100) return '$' + n.toFixed(2);
34
+ return '$' + Math.round(n).toLocaleString('en-US');
35
+ }
36
+ function fmtDuration(ms) {
37
+ var h = ms / 3600000;
38
+ if (h >= 48) return Math.round(h / 24) + 'd';
39
+ if (h >= 1) return h.toFixed(1) + 'h';
40
+ var m = ms / 60000;
41
+ return Math.max(1, Math.round(m)) + 'm';
42
+ }
43
+ function fmtTokens(n) {
44
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
45
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
46
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
47
+ return String(n || 0);
48
+ }
49
+ function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
50
+ function shortPath(p) { var parts = String(p || '').split('/'); return parts[parts.length - 1] || p; }
51
+
52
+ // ---- aggregation helpers ------------------------------------------------
53
+ function sum(arr, f) { var t = 0; for (var i = 0; i < arr.length; i++) t += f(arr[i]) || 0; return t; }
54
+ function groupBy(arr, keyFn) {
55
+ var m = new Map();
56
+ for (var i = 0; i < arr.length; i++) {
57
+ var k = keyFn(arr[i]);
58
+ if (!m.has(k)) m.set(k, []);
59
+ m.get(k).push(arr[i]);
60
+ }
61
+ return m;
62
+ }
63
+ function successRate(fs) {
64
+ var tot = sum(fs, function (s) { return s.toolCallsTotal; });
65
+ var ok = sum(fs, function (s) { return s.toolCallsSuccess; });
66
+ return tot ? Math.round((ok / tot) * 1000) / 10 : 0;
67
+ }
68
+
69
+ // ---- filter state -------------------------------------------------------
70
+ var NOW = Date.parse(DATA.meta.generatedAt) || Date.now();
71
+ // from/to hold epoch-ms local-day bounds; when either is set they override the preset.
72
+ var state = { range: 'all', from: null, to: null, agents: new Set(DATA.meta.agents), project: 'all', view: 'overview' };
73
+ var charts = [];
74
+ function destroyCharts() { for (var i = 0; i < charts.length; i++) { try { charts[i].destroy(); } catch (e) {} } charts = []; }
75
+
76
+ function startOfLocalDay(ms) { var d = new Date(ms); d.setHours(0, 0, 0, 0); return d.getTime(); }
77
+ // Parse a YYYY-MM-DD value as LOCAL midnight (or local end-of-day) — NOT UTC — so it
78
+ // lines up with the local-day bucketing used everywhere else (dayKey, heatmap, hours).
79
+ function parseLocalDate(str, endOfDay) {
80
+ if (!str) return null;
81
+ var p = String(str).split('-');
82
+ if (p.length !== 3) return null;
83
+ var y = +p[0], m = +p[1], d = +p[2];
84
+ if (!y || !m || !d) return null;
85
+ return endOfDay ? new Date(y, m - 1, d, 23, 59, 59, 999).getTime() : new Date(y, m - 1, d).getTime();
86
+ }
87
+
88
+ function rangeBounds() {
89
+ if (state.from != null || state.to != null) {
90
+ return { min: state.from != null ? state.from : 0, max: state.to != null ? state.to : Infinity };
91
+ }
92
+ if (state.range === 'today') return { min: startOfLocalDay(NOW), max: Infinity };
93
+ var spanDays = { '7d': 7, '30d': 30, '90d': 90 }[state.range];
94
+ return { min: spanDays ? NOW - spanDays * 86400000 : 0, max: Infinity };
95
+ }
96
+
97
+ function filtered() {
98
+ var b = rangeBounds();
99
+ return DATA.sessions.filter(function (s) {
100
+ return state.agents.has(s.agentName) &&
101
+ (state.project === 'all' || s.project === state.project) &&
102
+ s.startTime >= b.min && s.startTime <= b.max;
103
+ });
104
+ }
105
+
106
+ // Human label for the active client-side range (reflects live filters, not the
107
+ // static generation-time meta.rangeLabel) so the applied range is always visible.
108
+ function activeRangeLabel() {
109
+ if (state.from != null || state.to != null) {
110
+ return (state.from != null ? dayKey(state.from) : '…') + ' → ' + (state.to != null ? dayKey(state.to) : '…');
111
+ }
112
+ return { today: 'today', '7d': 'last 7d', '30d': 'last 30d', '90d': 'last 90d' }[state.range] || 'all';
113
+ }
114
+
115
+ // ---- chart factory ------------------------------------------------------
116
+ var GRID = 'rgba(255,255,255,0.06)';
117
+ function cssVar(name, fallback) {
118
+ try {
119
+ var v = getComputedStyle(document.documentElement).getPropertyValue(name); // may have leading space
120
+ return (v && v.trim()) || fallback;
121
+ } catch (e) { return fallback; }
122
+ }
123
+ // Recolor Chart.js for the active theme. Called at the start of every render so a
124
+ // theme switch re-renders charts with theme-correct text/grid colors.
125
+ function applyChartTheme() {
126
+ var light = document.documentElement.classList.contains('light');
127
+ GRID = light ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.06)';
128
+ if (window.Chart) {
129
+ Chart.defaults.color = cssVar('--color-text-muted', light ? '#5f6368' : '#9aa0a6');
130
+ Chart.defaults.font.family = 'Inter, sans-serif';
131
+ Chart.defaults.maintainAspectRatio = false;
132
+ }
133
+ }
134
+ function makeChart(canvas, config) {
135
+ if (!window.Chart) return null;
136
+ var c = new Chart(canvas, config);
137
+ charts.push(c);
138
+ return c;
139
+ }
140
+ function canvasIn(parent, height) {
141
+ var box = document.createElement('div');
142
+ box.className = 'chart-box';
143
+ if (height) box.style.height = height + 'px';
144
+ var cv = document.createElement('canvas');
145
+ box.appendChild(cv);
146
+ parent.appendChild(box);
147
+ return cv;
148
+ }
149
+
150
+ // ---- small DOM builders -------------------------------------------------
151
+ function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
152
+ function card(title, sub) {
153
+ var c = el('div', 'card');
154
+ var head = el('div', 'card-header');
155
+ head.appendChild(el('div', 'card-title', esc(title)));
156
+ if (sub) head.appendChild(el('span', 'text-muted', esc(sub)));
157
+ c.appendChild(head);
158
+ var body = el('div', 'card-body');
159
+ c.appendChild(body);
160
+ c._body = body;
161
+ return c;
162
+ }
163
+ function dayKey(ms) { var d = new Date(ms); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
164
+ function dayBuckets(fs, valueFn) {
165
+ var m = new Map();
166
+ fs.forEach(function (s) { var k = dayKey(s.startTime); m.set(k, (m.get(k) || 0) + (valueFn ? valueFn(s) : 1)); });
167
+ var keys = Array.from(m.keys()).sort();
168
+ return { labels: keys, values: keys.map(function (k) { return m.get(k); }) };
169
+ }
170
+
171
+ // ===================================================================== VIEWS
172
+ var VIEWS = {};
173
+
174
+ VIEWS.overview = function (host, fs) {
175
+ host.appendChild(el('h2', 'view-title', 'Overview'));
176
+ host.appendChild(el('p', 'view-sub', fs.length + ' sessions in view · ' + esc(activeRangeLabel()) + ' range'));
177
+
178
+ var totalCost = sum(fs, function (s) { return s.costUSD; });
179
+ var priced = DATA.meta.totals.pricedSessions;
180
+ var kpis = [
181
+ ['Sessions', fmtNum(fs.length), ''],
182
+ ['Duration', fmtDuration(sum(fs, function (s) { return s.durationMs; })), 'wall-clock span'],
183
+ ['Turns', fmtNum(sum(fs, function (s) { return s.turns; })), fs.length ? (Math.round(sum(fs, function (s) { return s.turns; }) / fs.length) + ' / session') : ''],
184
+ ['Files touched', fmtNum(sum(fs, function (s) { return s.fileOps; })), 'net ' + (sum(fs, function (s) { return s.netLines; }) >= 0 ? '+' : '') + fmtNum(sum(fs, function (s) { return s.netLines; })) + ' lines'],
185
+ ['Tool calls', fmtNum(sum(fs, function (s) { return s.toolCallsTotal; })), successRate(fs) + '% success'],
186
+ ['Est. cost', totalCost ? fmtUSD(totalCost) : '—', priced < DATA.meta.totals.sessions ? ('priced ' + priced + '/' + DATA.meta.totals.sessions) : 'tokens × pricing']
187
+ ];
188
+ var grid = el('div', 'kpi-grid');
189
+ kpis.forEach(function (k) {
190
+ var c = el('div', 'kpi' + (k[0] === 'Est. cost' && !totalCost ? ' soon' : ''));
191
+ c.appendChild(el('div', 'kpi-label', k[0]));
192
+ c.appendChild(el('div', 'kpi-value', k[1]));
193
+ if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2]));
194
+ grid.appendChild(c);
195
+ });
196
+ host.appendChild(grid);
197
+
198
+ // token-usage summary — input / output / cache write / cache read / total.
199
+ // "cache write" = cacheCreation (tokens written to the prompt cache),
200
+ // "cache read" = cacheRead (tokens served from the prompt cache).
201
+ var tIn = sum(fs, function (s) { return s.tokens ? s.tokens.input : 0; });
202
+ var tOut = sum(fs, function (s) { return s.tokens ? s.tokens.output : 0; });
203
+ var tcWrite = sum(fs, function (s) { return s.tokens ? s.tokens.cacheCreation : 0; });
204
+ var tcRead = sum(fs, function (s) { return s.tokens ? s.tokens.cacheRead : 0; });
205
+ var tTotal = sum(fs, function (s) { return s.tokens ? s.tokens.total : 0; });
206
+ var tkv = function (v) { return tTotal > 0 ? fmtTokens(v) : '—'; };
207
+ var tokenKpis = [
208
+ ['Input tokens', tkv(tIn), 'prompts sent to the model'],
209
+ ['Output tokens', tkv(tOut), 'completions generated'],
210
+ ['Cache write', tkv(tcWrite), 'tokens written to cache'],
211
+ ['Cache read', tkv(tcRead), 'tokens served from cache'],
212
+ ['Total tokens', tkv(tTotal), priced < DATA.meta.totals.sessions ? ('priced ' + priced + '/' + DATA.meta.totals.sessions + ' sessions') : 'across sessions in view']
213
+ ];
214
+ host.appendChild(el('div', 'kpi-section-label', 'Token usage'));
215
+ var tgrid = el('div', 'kpi-grid kpi-grid-tokens');
216
+ tokenKpis.forEach(function (k) {
217
+ var c = el('div', 'kpi');
218
+ c.appendChild(el('div', 'kpi-label', k[0]));
219
+ c.appendChild(el('div', 'kpi-value' + (tTotal > 0 ? '' : ' muted'), k[1]));
220
+ if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2]));
221
+ tgrid.appendChild(c);
222
+ });
223
+ host.appendChild(tgrid);
224
+
225
+ var row = el('div', 'grid-32 mb16');
226
+ var trend = card('Net lines over time');
227
+ row.appendChild(trend);
228
+ var modelsCard = card('Sessions by model');
229
+ row.appendChild(modelsCard);
230
+ host.appendChild(row);
231
+
232
+ var buckets = dayBuckets(fs, function (s) { return s.netLines; });
233
+ makeChart(canvasIn(trend._body), {
234
+ type: 'line',
235
+ data: { labels: buckets.labels, datasets: [{ label: 'Net lines', data: buckets.values, fill: true, borderColor: '#2297F6', backgroundColor: 'rgba(34,151,246,0.15)', tension: 0.3, pointRadius: 1 }] },
236
+ options: { plugins: { legend: { display: false } }, scales: { y: { grid: { color: GRID } }, x: { grid: { display: false }, ticks: { maxTicksLimit: 8 } } } }
237
+ });
238
+
239
+ var byModel = groupBy(fs, function (s) { return s.models[0] || 'unknown'; });
240
+ var mLabels = Array.from(byModel.keys()), mVals = mLabels.map(function (k) { return byModel.get(k).length; });
241
+ makeChart(canvasIn(modelsCard._body), {
242
+ type: 'doughnut',
243
+ data: { labels: mLabels, datasets: [{ data: mVals, backgroundColor: mLabels.map(function (_, i) { return PALETTE[i % PALETTE.length]; }), borderWidth: 0 }] },
244
+ options: { cutout: '62%', plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10 } } } }
245
+ });
246
+
247
+ // top projects
248
+ var proj = groupBy(fs, function (s) { return s.project; });
249
+ var pc = card('Top projects');
250
+ var rows = Array.from(proj.entries()).map(function (e) { return { p: e[0], sessions: e[1].length, net: sum(e[1], function (s) { return s.netLines; }) }; })
251
+ .sort(function (a, b) { return b.sessions - a.sessions; }).slice(0, 8);
252
+ pc._body.innerHTML = tableHTML(['Project', 'Sessions', 'Net lines'],
253
+ rows.map(function (r) { return ['<span title="' + esc(r.p) + '">' + esc(shortPath(r.p)) + '</span>', fmtNum(r.sessions), tdNum(r.net)]; }));
254
+ host.appendChild(pc);
255
+ };
256
+
257
+ VIEWS.agents = function (host, fs) {
258
+ host.appendChild(el('h2', 'view-title', 'Agents · Compare'));
259
+ host.appendChild(el('p', 'view-sub', 'Side-by-side across coding agents. Toggle agents in the top bar to add/remove.'));
260
+ if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions for the selected agents/range.')); return; }
261
+
262
+ var byAgent = groupBy(fs, function (s) { return s.agentName; });
263
+ var agentList = Array.from(byAgent.keys());
264
+
265
+ var cols = el('div', 'agent-cols');
266
+ agentList.forEach(function (a) {
267
+ var ss = byAgent.get(a);
268
+ var c = el('div', 'acard'); c.style.setProperty('--ac', colorFor(a));
269
+ c.appendChild(el('h3', null, '<span class="pill"></span>' + esc(a)));
270
+ c.appendChild(el('span', 'text-muted', '<span style="font-size:12px">' + ss.length + ' sessions · ' + Math.round((ss.length / fs.length) * 100) + '% of activity</span>'));
271
+ var mini = el('div', 'mini');
272
+ var stats = [
273
+ ['Net lines', (sum(ss, function (s) { return s.netLines; }) >= 0 ? '+' : '') + fmtNum(sum(ss, function (s) { return s.netLines; }))],
274
+ ['Turns', fmtNum(sum(ss, function (s) { return s.turns; }))],
275
+ ['Tool success', successRate(ss) + '%'],
276
+ ['Avg session', fmtDuration(sum(ss, function (s) { return s.durationMs; }) / ss.length)]
277
+ ];
278
+ stats.forEach(function (st) { var d = el('div'); d.appendChild(el('div', 'l', st[0])); d.appendChild(el('div', 'v', st[1])); mini.appendChild(d); });
279
+ c.appendChild(mini);
280
+ cols.appendChild(c);
281
+ });
282
+ host.appendChild(cols);
283
+
284
+ var row = el('div', 'grid-2 mb16');
285
+ var stackCard = card('Sessions over time — by agent');
286
+ var shareCard = card('Share of net lines');
287
+ row.appendChild(stackCard); row.appendChild(shareCard);
288
+ host.appendChild(row);
289
+
290
+ // stacked sessions/day by agent
291
+ var allDays = Array.from(new Set(fs.map(function (s) { return dayKey(s.startTime); }))).sort();
292
+ var datasets = agentList.map(function (a) {
293
+ var ss = byAgent.get(a);
294
+ var perDay = {}; ss.forEach(function (s) { var k = dayKey(s.startTime); perDay[k] = (perDay[k] || 0) + 1; });
295
+ return { label: a, data: allDays.map(function (d) { return perDay[d] || 0; }), backgroundColor: colorFor(a) };
296
+ });
297
+ makeChart(canvasIn(stackCard._body), {
298
+ type: 'bar', data: { labels: allDays, datasets: datasets },
299
+ options: { plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10 } } }, scales: { x: { stacked: true, grid: { display: false }, ticks: { maxTicksLimit: 8 } }, y: { stacked: true, grid: { color: GRID } } } }
300
+ });
301
+
302
+ makeChart(canvasIn(shareCard._body), {
303
+ type: 'doughnut',
304
+ data: { labels: agentList, datasets: [{ data: agentList.map(function (a) { return Math.abs(sum(byAgent.get(a), function (s) { return s.netLines; })); }), backgroundColor: agentList.map(colorFor), borderWidth: 0 }] },
305
+ options: { cutout: '60%', plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10 } } } }
306
+ });
307
+
308
+ var detail = card('Per-agent detail');
309
+ // Agent + Top model are categorical (left); the rest are numeric (right). Passing an
310
+ // explicit mask keeps the text "Top model" column left-aligned instead of being treated
311
+ // as numeric by the default "everything but column 0" rule.
312
+ detail._body.innerHTML = tableHTML(['Agent', 'Sessions', 'Turns', 'Files', 'Net lines', 'Top model', 'Tool success', 'Cost'],
313
+ agentList.map(function (a) {
314
+ var ss = byAgent.get(a);
315
+ var topModel = topOf(ss.flatMap(function (s) { return s.models; }));
316
+ return ['<span class="tag tag-sm" style="text-transform:capitalize">' + esc(a) + '</span>', fmtNum(ss.length), tdNum(sum(ss, function (s) { return s.turns; })), tdNum(sum(ss, function (s) { return s.fileOps; })), tdNum(sum(ss, function (s) { return s.netLines; })), '<span class="tag tag-sm">' + esc(topModel || '—') + '</span>', tdNum(successRate(ss) + '%'), tdNum(fmtUSD(sum(ss, function (s) { return s.costUSD; })))];
317
+ }),
318
+ [false, true, true, true, true, false, true, true]);
319
+ host.appendChild(detail);
320
+ };
321
+
322
+ VIEWS.projects = function (host, fs) {
323
+ host.appendChild(el('h2', 'view-title', 'Projects'));
324
+ host.appendChild(el('p', 'view-sub', 'Click a project to expand its branches & sessions.'));
325
+ if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions in view.')); return; }
326
+ var byProj = groupBy(fs, function (s) { return s.project; });
327
+ var c = card('Projects');
328
+ var wrap = el('div', 'table-wrapper');
329
+ var rows = Array.from(byProj.entries()).map(function (e) { return { p: e[0], ss: e[1] }; })
330
+ .sort(function (a, b) { return b.ss.length - a.ss.length; });
331
+ var html = '<table class="table"><thead><tr><th>Project</th><th class="td-number">Sessions</th><th class="td-number">Turns</th><th class="td-number">Net lines</th><th class="td-number">Tool success</th><th class="td-number">Cost</th></tr></thead><tbody>';
332
+ rows.forEach(function (r, i) {
333
+ html += '<tr class="clickable" data-proj="' + i + '"><td>▸ ' + esc(shortPath(r.p)) + '</td><td class="td-number">' + fmtNum(r.ss.length) + '</td><td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.turns; })) + '</td><td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(r.ss) + '%</td><td class="td-number">' + fmtUSD(sum(r.ss, function (s) { return s.costUSD; })) + '</td></tr>';
334
+ // branch sub-rows (hidden)
335
+ var byBranch = groupBy(r.ss, function (s) { return s.branch || '(none)'; });
336
+ byBranch.forEach(function (bss, b) {
337
+ html += '<tr class="drill" data-parent="' + i + '" style="display:none"><td style="padding-left:28px">⎇ ' + esc(b) + '</td><td class="td-number">' + bss.length + '</td><td class="td-number">' + fmtNum(sum(bss, function (s) { return s.turns; })) + '</td><td class="td-number">' + fmtNum(sum(bss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(bss) + '%</td><td class="td-number">' + fmtUSD(sum(bss, function (s) { return s.costUSD; })) + '</td></tr>';
338
+ });
339
+ });
340
+ html += '</tbody></table>';
341
+ wrap.innerHTML = html;
342
+ wrap.addEventListener('click', function (ev) {
343
+ var tr = ev.target.closest('tr[data-proj]');
344
+ if (!tr) return;
345
+ var id = tr.getAttribute('data-proj');
346
+ wrap.querySelectorAll('tr[data-parent="' + id + '"]').forEach(function (sub) { sub.style.display = sub.style.display === 'none' ? '' : 'none'; });
347
+ });
348
+ c._body.style.paddingTop = '0';
349
+ c._body.appendChild(wrap);
350
+ host.appendChild(c);
351
+ };
352
+
353
+ VIEWS.toolsmodels = function (host, fs) {
354
+ host.appendChild(el('h2', 'view-title', 'Tools & Models'));
355
+ host.appendChild(el('p', 'view-sub', 'Tool usage across sessions and model token/cost distribution.'));
356
+ if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions in view.')); return; }
357
+
358
+ // aggregate tools across sessions
359
+ var toolAgg = new Map();
360
+ fs.forEach(function (s) {
361
+ (s.tools || []).forEach(function (t) {
362
+ var cur = toolAgg.get(t.toolName) || { total: 0, success: 0, failure: 0 };
363
+ cur.total += t.totalCalls; cur.success += t.successCount; cur.failure += t.failureCount;
364
+ toolAgg.set(t.toolName, cur);
365
+ });
366
+ });
367
+ var tools = Array.from(toolAgg.entries()).map(function (e) { return { name: e[0], total: e[1].total, success: e[1].success, rate: e[1].total ? Math.round((e[1].success / e[1].total) * 100) : 0 }; })
368
+ .sort(function (a, b) { return b.total - a.total; });
369
+ var maxTool = tools.length ? tools[0].total : 1;
370
+
371
+ var row = el('div', 'grid-2 mb16');
372
+ var toolCard = card('Tool usage & success rate');
373
+ tools.slice(0, 12).forEach(function (t) {
374
+ var bar = el('div', 'bar-row');
375
+ var color = t.rate >= 90 ? '#259F4C' : (t.rate >= 70 ? '#F5A534' : '#F9303C');
376
+ bar.innerHTML = '<span class="nm">' + esc(t.name) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + Math.max(3, (t.total / maxTool) * 100) + '%;background:' + color + '"></div></div><span class="vl">' + fmtNum(t.total) + ' · ' + t.rate + '%</span>';
377
+ toolCard._body.appendChild(bar);
378
+ });
379
+ if (!tools.length) toolCard._body.appendChild(el('div', 'empty', 'No per-tool data.'));
380
+ row.appendChild(toolCard);
381
+
382
+ // models by tokens (from perModelCost)
383
+ var modelAgg = new Map();
384
+ fs.forEach(function (s) { (s.perModelCost || []).forEach(function (m) { modelAgg.set(m.model, (modelAgg.get(m.model) || 0) + (m.tokens ? m.tokens.total : 0)); }); });
385
+ var modelCard = card('Tokens by model');
386
+ var mEntries = Array.from(modelAgg.entries()).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 8);
387
+ if (mEntries.length) {
388
+ makeChart(canvasIn(modelCard._body), {
389
+ type: 'bar',
390
+ data: { labels: mEntries.map(function (e) { return e[0]; }), datasets: [{ data: mEntries.map(function (e) { return e[1]; }), backgroundColor: mEntries.map(function (_, i) { return PALETTE[i % PALETTE.length]; }), borderRadius: 5 }] },
391
+ options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (c) { return fmtTokens(c.parsed.x) + ' tokens'; } } } }, scales: { x: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtTokens(v); } } }, y: { grid: { display: false } } } }
392
+ });
393
+ } else {
394
+ modelCard._body.appendChild(el('div', 'empty', 'No token data (sessions unpriced or no native logs).'));
395
+ }
396
+ row.appendChild(modelCard);
397
+ host.appendChild(row);
398
+ };
399
+
400
+ VIEWS.activity = function (host, fs) {
401
+ host.appendChild(el('h2', 'view-title', 'Activity'));
402
+ host.appendChild(el('p', 'view-sub', 'When sessions happen — by weekday and hour (local time).'));
403
+ if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions in view.')); return; }
404
+
405
+ var days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
406
+ var grid = {}; var maxCell = 0;
407
+ fs.forEach(function (s) {
408
+ var d = new Date(s.startTime);
409
+ var di = (d.getDay() + 6) % 7; // Mon=0
410
+ var h = d.getHours();
411
+ var key = di + '_' + h;
412
+ grid[key] = (grid[key] || 0) + 1;
413
+ if (grid[key] > maxCell) maxCell = grid[key];
414
+ });
415
+ var hmCard = card('Sessions by weekday × hour');
416
+ var hm = el('div', 'heatmap');
417
+ var html = '<div></div>'; // build the whole grid as one string, then assign once
418
+ for (var h = 0; h < 24; h++) html += '<div class="heat-hr">' + (h % 6 === 0 ? h : '') + '</div>';
419
+ days.forEach(function (dn, di) {
420
+ html += '<div class="heat-lbl">' + dn + '</div>';
421
+ for (var hr = 0; hr < 24; hr++) {
422
+ var v = grid[di + '_' + hr] || 0;
423
+ var a = maxCell ? (v / maxCell) : 0;
424
+ html += '<div class="heat-cell" title="' + dn + ' ' + hr + ':00 — ' + v + ' sessions" style="background:rgba(34,151,246,' + (v ? (0.15 + a * 0.85).toFixed(2) : 0) + ')"></div>';
425
+ }
426
+ });
427
+ hm.innerHTML = html;
428
+ hmCard._body.appendChild(hm);
429
+ host.appendChild(hmCard);
430
+
431
+ var row = el('div', 'grid-2');
432
+ var hourCard = card('By hour of day');
433
+ var hours = []; for (var i = 0; i < 24; i++) hours.push(0);
434
+ fs.forEach(function (s) { hours[new Date(s.startTime).getHours()]++; });
435
+ makeChart(canvasIn(hourCard._body), {
436
+ type: 'bar', data: { labels: hours.map(function (_, i) { return i; }), datasets: [{ data: hours, backgroundColor: '#2297F6', borderRadius: 3 }] },
437
+ options: { plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: GRID } } } }
438
+ });
439
+ row.appendChild(hourCard);
440
+
441
+ var wdCard = card('By weekday');
442
+ var wd = [0, 0, 0, 0, 0, 0, 0];
443
+ fs.forEach(function (s) { wd[(new Date(s.startTime).getDay() + 6) % 7]++; });
444
+ makeChart(canvasIn(wdCard._body), {
445
+ type: 'bar', data: { labels: days, datasets: [{ data: wd, backgroundColor: '#7C5CFC', borderRadius: 3 }] },
446
+ options: { plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: GRID } } } }
447
+ });
448
+ row.appendChild(wdCard);
449
+ host.appendChild(row);
450
+ };
451
+
452
+ VIEWS.cost = function (host, fs) {
453
+ host.appendChild(el('h2', 'view-title', 'Cost'));
454
+ host.appendChild(el('p', 'view-sub', 'Estimated cost (API-equivalent) — token usage × model pricing. On a subscription you don’t pay per token; this is the equivalent metered API value.'));
455
+
456
+ var total = sum(fs, function (s) { return s.costUSD; });
457
+ var priced = DATA.meta.totals.pricedSessions, totalSessions = DATA.meta.totals.sessions;
458
+
459
+ var banner = el('div', 'alert ' + (priced < totalSessions ? 'alert-warning' : 'alert-info'));
460
+ var msg = 'Priced ' + priced + ' of ' + totalSessions + ' sessions with recoverable token usage. '
461
+ + 'The rest have no readable native log — coding agents rotate/delete old transcripts, so historical '
462
+ + 'token data is incomplete (this does not affect the cost of the sessions that are priced). See Coverage by agent below.';
463
+ if (DATA.meta.unpricedModels && DATA.meta.unpricedModels.length) msg += ' Unpriced models: ' + DATA.meta.unpricedModels.join(', ') + '.';
464
+ banner.textContent = msg; // textContent is safe — do not pre-escape (would double-escape)
465
+ host.appendChild(banner);
466
+
467
+ var grid = el('div', 'kpi-grid'); grid.style.gridTemplateColumns = 'repeat(3,1fr)';
468
+ var tok = fs.reduce(function (acc, s) { return acc + (s.tokens ? s.tokens.total : 0); }, 0);
469
+ [['Total est. cost', fmtUSD(total)], ['Total tokens', fmtTokens(tok)], ['Avg cost / session', fs.length ? fmtUSD(total / fs.length) : '—']].forEach(function (k) {
470
+ var c = el('div', 'kpi'); c.appendChild(el('div', 'kpi-label', k[0])); c.appendChild(el('div', 'kpi-value', k[1])); grid.appendChild(c);
471
+ });
472
+ host.appendChild(grid);
473
+
474
+ // per-agent coverage — answers "which tools' metrics are included?"
475
+ var cov = DATA.meta.coverage || [];
476
+ if (cov.length) {
477
+ var covCard = card('Coverage by agent', 'sessions with token data, per tool');
478
+ covCard._body.style.paddingTop = '0';
479
+ covCard._body.innerHTML = '<div class="table-wrapper">' + tableHTML(['Agent', 'Sessions', 'Priced', 'Native log', 'Status'],
480
+ cov.map(function (c) {
481
+ var status;
482
+ if (c.total > 0 && c.priced >= c.total) status = '<span class="cov-ok">✓ full</span>';
483
+ else if (c.withLog === 0) status = '<span class="cov-warn">no native log</span>';
484
+ else if (c.priced === 0) status = '<span class="cov-warn">no token reader</span>';
485
+ else status = '<span class="cov-warn">partial</span>';
486
+ return ['<span class="tag tag-sm" style="text-transform:capitalize">' + esc(c.agentName) + '</span>',
487
+ fmtNum(c.total), c.priced + '/' + c.total, fmtNum(c.withLog), status];
488
+ }),
489
+ [false, true, true, true, false]) + '</div>';
490
+ host.appendChild(covCard);
491
+ }
492
+
493
+ var row = el('div', 'grid-2 mb16');
494
+ var byAgentCard = card('Cost by agent');
495
+ var byAgent = groupBy(fs, function (s) { return s.agentName; });
496
+ var aList = Array.from(byAgent.keys());
497
+ makeChart(canvasIn(byAgentCard._body), {
498
+ type: 'doughnut', data: { labels: aList, datasets: [{ data: aList.map(function (a) { return sum(byAgent.get(a), function (s) { return s.costUSD; }); }), backgroundColor: aList.map(colorFor), borderWidth: 0 }] },
499
+ options: { cutout: '60%', plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10 } }, tooltip: { callbacks: { label: function (c) { return c.label + ': ' + fmtUSD(c.parsed); } } } } }
500
+ });
501
+ row.appendChild(byAgentCard);
502
+
503
+ var byModelCard = card('Cost by model');
504
+ var modelCost = new Map();
505
+ fs.forEach(function (s) { (s.perModelCost || []).forEach(function (m) { modelCost.set(m.model, (modelCost.get(m.model) || 0) + m.costUSD); }); });
506
+ var mc = Array.from(modelCost.entries()).filter(function (e) { return e[1] > 0; }).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 8);
507
+ if (mc.length) {
508
+ makeChart(canvasIn(byModelCard._body), {
509
+ type: 'bar', data: { labels: mc.map(function (e) { return e[0]; }), datasets: [{ data: mc.map(function (e) { return e[1]; }), backgroundColor: mc.map(function (_, i) { return PALETTE[i % PALETTE.length]; }), borderRadius: 5 }] },
510
+ options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (c) { return fmtUSD(c.parsed.x); } } } }, scales: { x: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtUSD(v); } } }, y: { grid: { display: false } } } }
511
+ });
512
+ } else { byModelCard._body.appendChild(el('div', 'empty', 'No priced model data.')); }
513
+ row.appendChild(byModelCard);
514
+ host.appendChild(row);
515
+
516
+ var topCard = card('Most expensive sessions');
517
+ var top = fs.slice().sort(function (a, b) { return b.costUSD - a.costUSD; }).slice(0, 10);
518
+ topCard._body.style.paddingTop = '0';
519
+ topCard._body.innerHTML = '<div class="table-wrapper">' + tableHTML(
520
+ ['Session', 'Agent', 'Project', 'Input', 'Output', 'Cached', 'Total', 'Cost'],
521
+ top.map(function (s) {
522
+ return [esc(s.sessionId.slice(0, 8)),
523
+ '<span class="tag tag-sm" style="text-transform:capitalize">' + esc(s.agentName) + '</span>',
524
+ '<span title="' + esc(s.project) + '">' + esc(shortPath(s.project)) + '</span>',
525
+ fmtTokens(tkIn(s)), fmtTokens(tkOut(s)), fmtTokens(tkCached(s)), fmtTokens(s.tokens ? s.tokens.total : 0), fmtUSD(s.costUSD)];
526
+ }),
527
+ [false, false, false, true, true, true, true, true]) + '</div>';
528
+ host.appendChild(topCard);
529
+ };
530
+
531
+ VIEWS.sessions = function (host, fs) {
532
+ host.appendChild(el('h2', 'view-title', 'Sessions'));
533
+ host.appendChild(el('p', 'view-sub', fs.length + ' sessions · search by project, agent, branch, or id.'));
534
+ var bar = el('div', 'mb16');
535
+ var input = el('input', 'input search-input'); input.placeholder = 'Search sessions…';
536
+ bar.appendChild(input); host.appendChild(bar);
537
+ var c = card('All sessions'); c._body.style.paddingTop = '0';
538
+ var holder = el('div', 'table-wrapper'); c._body.appendChild(holder); host.appendChild(c);
539
+
540
+ function draw(q) {
541
+ var list = fs.slice().sort(function (a, b) { return b.startTime - a.startTime; });
542
+ if (q) {
543
+ var ql = q.toLowerCase();
544
+ list = list.filter(function (s) { return (s.sessionId + ' ' + s.agentName + ' ' + s.project + ' ' + s.branch).toLowerCase().indexOf(ql) >= 0; });
545
+ }
546
+ holder.innerHTML = tableHTML(
547
+ ['Date', 'Agent', 'Project', 'Branch', 'Turns', 'Net lines', 'Input', 'Output', 'Cached', 'Cost'],
548
+ list.slice(0, 300).map(function (s) {
549
+ return [new Date(s.startTime).toISOString().slice(0, 16).replace('T', ' '),
550
+ '<span class="tag tag-sm" style="text-transform:capitalize">' + esc(s.agentName) + '</span>',
551
+ '<span title="' + esc(s.project) + '">' + esc(shortPath(s.project)) + '</span>', esc(s.branch || '—'),
552
+ fmtNum(s.turns), fmtNum(s.netLines), fmtTokens(tkIn(s)), fmtTokens(tkOut(s)), fmtTokens(tkCached(s)), fmtUSD(s.costUSD)];
553
+ }),
554
+ [false, false, false, false, true, true, true, true, true, true]);
555
+ if (list.length > 300) holder.appendChild(el('p', 'text-muted', '<span style="font-size:12px">Showing first 300 of ' + list.length + '.</span>'));
556
+ }
557
+ input.addEventListener('input', function () { draw(input.value.trim()); });
558
+ draw('');
559
+ };
560
+
561
+ // ---- table + misc helpers ----------------------------------------------
562
+ function tdNum(v) { return '<span class="td-number">' + (typeof v === 'number' ? fmtNum(v) : v) + '</span>'; }
563
+ // numericCols: optional boolean[] marking right-aligned numeric columns. Default = every
564
+ // column except the first (back-compat). Text columns (agent, project, branch, status) must
565
+ // be left-aligned, so callers with interleaved/leading text pass an explicit mask.
566
+ function tableHTML(headers, rows, numericCols) {
567
+ var isNum = numericCols || headers.map(function (_, i) { return i > 0; });
568
+ var h = '<table class="table"><thead><tr>';
569
+ headers.forEach(function (x, i) { h += '<th' + (isNum[i] ? ' class="td-number"' : '') + '>' + esc(x) + '</th>'; });
570
+ h += '</tr></thead><tbody>';
571
+ if (!rows.length) h += '<tr><td colspan="' + headers.length + '" class="text-muted">No data</td></tr>';
572
+ rows.forEach(function (r) { h += '<tr>' + r.map(function (cell, i) { return '<td' + (isNum[i] ? ' class="td-number"' : '') + '>' + cell + '</td>'; }).join('') + '</tr>'; });
573
+ return h + '</tbody></table>';
574
+ }
575
+ // tokens shorthand: cached = cacheRead + cacheCreation (the prompt-cache reuse + writes)
576
+ function tkIn(s) { return s.tokens ? s.tokens.input : 0; }
577
+ function tkOut(s) { return s.tokens ? s.tokens.output : 0; }
578
+ function tkCached(s) { return s.tokens ? (s.tokens.cacheRead + s.tokens.cacheCreation) : 0; }
579
+ function topOf(arr) { var m = {}; arr.forEach(function (x) { m[x] = (m[x] || 0) + 1; }); var best = null, bc = 0; for (var k in m) if (m[k] > bc) { bc = m[k]; best = k; } return best; }
580
+
581
+ // ---- render + controls --------------------------------------------------
582
+ function setTheme(light) {
583
+ document.documentElement.classList.toggle('light', !!light);
584
+ try { localStorage.setItem('codemie-analytics-theme', light ? 'light' : 'dark'); } catch (e) {}
585
+ render(); // recolor charts for the new theme
586
+ }
587
+
588
+ function render() {
589
+ applyChartTheme();
590
+ destroyCharts();
591
+ root.innerHTML = '';
592
+ (VIEWS[state.view] || VIEWS.overview)(root, filtered());
593
+ document.querySelectorAll('.nav-i').forEach(function (n) { n.classList.toggle('active', n.getAttribute('data-view') === state.view); });
594
+ }
595
+
596
+ function buildControls() {
597
+ // nav
598
+ document.querySelectorAll('.nav-i').forEach(function (n) {
599
+ n.addEventListener('click', function () { state.view = n.getAttribute('data-view'); render(); });
600
+ });
601
+ // range presets (incl. Today) — selecting one clears any custom date range
602
+ var dFrom = document.getElementById('date-from');
603
+ var dTo = document.getElementById('date-to');
604
+ document.querySelectorAll('#range-seg button').forEach(function (b) {
605
+ b.addEventListener('click', function () {
606
+ state.range = b.getAttribute('data-range');
607
+ state.from = null; state.to = null;
608
+ if (dFrom) dFrom.value = '';
609
+ if (dTo) dTo.value = '';
610
+ document.querySelectorAll('#range-seg button').forEach(function (x) { x.classList.toggle('on', x === b); });
611
+ render();
612
+ });
613
+ });
614
+ // custom date range — applies on change, deactivates the preset segment
615
+ function onDateChange() {
616
+ state.from = parseLocalDate(dFrom && dFrom.value, false);
617
+ state.to = parseLocalDate(dTo && dTo.value, true);
618
+ if (state.from != null || state.to != null) {
619
+ state.range = 'custom';
620
+ document.querySelectorAll('#range-seg button').forEach(function (x) { x.classList.remove('on'); });
621
+ }
622
+ render();
623
+ }
624
+ if (dFrom) dFrom.addEventListener('change', onDateChange);
625
+ if (dTo) dTo.addEventListener('change', onDateChange);
626
+ var dClear = document.getElementById('date-clear');
627
+ if (dClear) dClear.addEventListener('click', function () {
628
+ state.from = null; state.to = null;
629
+ if (dFrom) dFrom.value = '';
630
+ if (dTo) dTo.value = '';
631
+ state.range = 'all';
632
+ document.querySelectorAll('#range-seg button').forEach(function (x) { x.classList.toggle('on', x.getAttribute('data-range') === 'all'); });
633
+ render();
634
+ });
635
+ // agent chips
636
+ var chips = document.getElementById('agent-chips');
637
+ DATA.meta.agents.forEach(function (a) {
638
+ var chip = el('span', 'chip-tog');
639
+ chip.innerHTML = '<span class="dot" style="background:' + colorFor(a) + '"></span>' + esc(a);
640
+ chip.addEventListener('click', function () {
641
+ if (state.agents.has(a)) { state.agents.delete(a); chip.classList.add('off'); }
642
+ else { state.agents.add(a); chip.classList.remove('off'); }
643
+ render();
644
+ });
645
+ chips.appendChild(chip);
646
+ });
647
+ // project select
648
+ var sel = document.getElementById('project-select');
649
+ var projects = Array.from(new Set(DATA.sessions.map(function (s) { return s.project; }))).sort();
650
+ sel.innerHTML = '<option value="all">All projects (' + projects.length + ')</option>' + projects.map(function (p) { return '<option value="' + esc(p) + '">' + esc(shortPath(p)) + '</option>'; }).join('');
651
+ sel.addEventListener('change', function () { state.project = sel.value; render(); });
652
+ // footer
653
+ document.getElementById('side-foot').innerHTML = fmtNum(DATA.meta.totals.sessions) + ' sessions<br>' + DATA.meta.agents.length + ' agents<br>generated ' + esc((DATA.meta.generatedAt || '').slice(0, 10));
654
+ // theme switch (bottom-left)
655
+ var sw = document.getElementById('theme-switch');
656
+ if (sw) {
657
+ var label = sw.querySelector('.ts-text');
658
+ function syncThemeText() { if (label) label.textContent = document.documentElement.classList.contains('light') ? 'Light' : 'Dark'; }
659
+ function toggleTheme() { setTheme(!document.documentElement.classList.contains('light')); syncThemeText(); }
660
+ sw.addEventListener('click', toggleTheme);
661
+ sw.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleTheme(); } });
662
+ syncThemeText();
663
+ }
664
+ }
665
+
666
+ // apply saved theme before first paint. Default = dark (CodeMie's product default);
667
+ // only an explicit saved choice flips it to light.
668
+ (function initTheme() {
669
+ var saved = null;
670
+ try { saved = localStorage.getItem('codemie-analytics-theme'); } catch (e) {}
671
+ document.documentElement.classList.toggle('light', saved === 'light');
672
+ })();
673
+
674
+ buildControls();
675
+ render();
676
+ })();