agentlytics 0.1.9 → 0.1.11

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.
package/relay-client.js CHANGED
@@ -1,9 +1,5 @@
1
1
  const chalk = require('chalk');
2
2
  const http = require('http');
3
- const https = require('https');
4
- const path = require('path');
5
- const os = require('os');
6
- const fs = require('fs');
7
3
  const readline = require('readline');
8
4
  const crypto = require('crypto');
9
5
 
package/server.js CHANGED
@@ -266,7 +266,16 @@ app.get('/api/share-image', (req, res) => {
266
266
  try {
267
267
  const overview = cache.getCachedOverview();
268
268
  const stats = cache.getCachedDashboardStats();
269
- const svg = generateShareSvg(overview, stats);
269
+ const costs = cache.getCostAnalytics({ hiddenFolders: getHiddenFolders() });
270
+ const opts = {};
271
+ if (req.query.showEditors !== undefined) opts.showEditors = req.query.showEditors !== 'false';
272
+ if (req.query.showModels !== undefined) opts.showModels = req.query.showModels !== 'false';
273
+ if (req.query.showCosts !== undefined) opts.showCosts = req.query.showCosts !== 'false';
274
+ if (req.query.showTokens !== undefined) opts.showTokens = req.query.showTokens !== 'false';
275
+ if (req.query.showHours !== undefined) opts.showHours = req.query.showHours !== 'false';
276
+ if (req.query.username) opts.username = req.query.username;
277
+ if (req.query.theme) opts.theme = req.query.theme;
278
+ const svg = generateShareSvg(overview, stats, costs, opts);
270
279
  res.setHeader('Content-Type', 'image/svg+xml');
271
280
  res.send(svg);
272
281
  } catch (err) {
package/share-image.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Generates a shareable SVG stats card from cached data.
3
+ * Accepts an `opts` object to toggle sections on/off.
3
4
  */
4
5
 
5
6
  function fmt(n) {
@@ -9,6 +10,14 @@ function fmt(n) {
9
10
  return n.toLocaleString();
10
11
  }
11
12
 
13
+ function fmtCost(n) {
14
+ if (n == null || n === 0) return '$0';
15
+ if (n < 0.01) return '<$0.01';
16
+ if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'K';
17
+ if (n >= 100) return '$' + Math.round(n);
18
+ return '$' + n.toFixed(2);
19
+ }
20
+
12
21
  const EDITOR_COLORS = {
13
22
  'cursor': '#f59e0b',
14
23
  'windsurf': '#06b6d4',
@@ -20,9 +29,11 @@ const EDITOR_COLORS = {
20
29
  'vscode-insiders': '#60a5fa',
21
30
  'zed': '#10b981',
22
31
  'opencode': '#ec4899',
32
+ 'codex': '#0f766e',
23
33
  'gemini-cli': '#4285f4',
24
34
  'copilot-cli': '#8957e5',
25
35
  'cursor-agent': '#f59e0b',
36
+ 'commandcode': '#e11d48',
26
37
  };
27
38
 
28
39
  const EDITOR_LABELS = {
@@ -36,107 +47,309 @@ const EDITOR_LABELS = {
36
47
  'vscode-insiders': 'VS Code Ins.',
37
48
  'zed': 'Zed',
38
49
  'opencode': 'OpenCode',
50
+ 'codex': 'Codex',
39
51
  'gemini-cli': 'Gemini CLI',
40
52
  'copilot-cli': 'Copilot CLI',
41
53
  'cursor-agent': 'Cursor Agent',
54
+ 'commandcode': 'Cmd Code',
42
55
  };
43
56
 
44
- function generateShareSvg(overview, stats) {
45
- const W = 800, H = 440;
46
- const F = "Menlo, Monaco, Cascadia Code, Courier New, monospace";
57
+ /**
58
+ * @param {object} overview — from getCachedOverview()
59
+ * @param {object} stats — from getCachedDashboardStats()
60
+ * @param {object} costs — from getCostBreakdown()
61
+ * @param {object} [opts] — toggle sections
62
+ * @param {boolean} [opts.showEditors=true]
63
+ * @param {boolean} [opts.showModels=true]
64
+ * @param {boolean} [opts.showCosts=true]
65
+ * @param {boolean} [opts.showTokens=true]
66
+ * @param {boolean} [opts.showHours=true]
67
+ * @param {boolean} [opts.showProjects=true]
68
+ * @param {string} [opts.username]
69
+ * @param {string} [opts.theme='dark'] — 'dark' or 'light'
70
+ */
71
+ const THEMES = {
72
+ dark: {
73
+ bg: '#09090f', bg2: '#111118', card: '#111', border: '#1e1e2a',
74
+ text: '#fff', text2: '#888', text3: '#666', text4: '#555', text5: '#444',
75
+ titleText: '#555',
76
+ hourHigh: '#818cf8', hourMed: '#6366f1', hourLow: '#4f46e5', hourMin: '#1e1b4b',
77
+ },
78
+ light: {
79
+ bg: '#f8f8fa', bg2: '#eeeef2', card: '#e8e8ee', border: '#d0d0d8',
80
+ text: '#111', text2: '#555', text3: '#888', text4: '#999', text5: '#aaa',
81
+ titleText: '#888',
82
+ hourHigh: '#6366f1', hourMed: '#818cf8', hourLow: '#a5b4fc', hourMin: '#e0e7ff',
83
+ },
84
+ };
85
+
86
+ function generateShareSvg(overview, stats, costs, opts = {}) {
87
+ const show = {
88
+ editors: opts.showEditors !== false,
89
+ models: opts.showModels !== false,
90
+ costs: opts.showCosts !== false,
91
+ tokens: opts.showTokens !== false,
92
+ hours: opts.showHours !== false,
93
+ };
94
+ const username = opts.username || '';
95
+ const t = THEMES[opts.theme] || THEMES.dark;
96
+
97
+ const W = 1200;
98
+ const H_FIXED = 675;
99
+ const F = "'Menlo','Monaco','Cascadia Code','Courier New',monospace";
47
100
  const editors = overview.editors || [];
48
101
  const tk = stats.tokens || {};
49
102
  const streaks = stats.streaks || {};
50
103
  const topModels = (stats.topModels || []).slice(0, 5);
104
+ const costData = costs || {};
105
+ const totalCost = costData.totalCost || 0;
106
+ const costByEditor = (costData.byEditor || []).slice(0, 6);
107
+ const totalTokens = (tk.input || 0) + (tk.output || 0);
108
+ const now = new Date();
109
+ const dateStr = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
110
+
111
+ // ── Compute layout ──
112
+ let y = 0;
113
+ const pad = 28;
114
+
115
+ // Title bar
116
+ y += 40;
117
+ // Header area (prompt + branding)
118
+ y += 54;
119
+ // KPI row
120
+ y += 80;
121
+ // Sections
122
+ const midSections = [];
123
+ if (show.editors) midSections.push('editors');
124
+ if (show.costs && totalCost > 0) midSections.push('costs');
125
+ if (show.hours) midSections.push('hours');
126
+ if (show.models && topModels.length > 0) midSections.push('models');
127
+
128
+ // Layout: 2 columns, each section ~row
129
+ const leftSections = [];
130
+ const rightSections = [];
131
+ midSections.forEach((s, i) => {
132
+ if (i % 2 === 0) leftSections.push(s);
133
+ else rightSections.push(s);
134
+ });
135
+
136
+ function sectionHeight(name) {
137
+ if (name === 'editors') return Math.max(editors.slice(0, 8).length * 26 + 34, 70);
138
+ if (name === 'costs') return Math.max(costByEditor.length * 26 + 34, 70);
139
+ if (name === 'hours') return 110;
140
+ if (name === 'models') return topModels.length * 22 + 34;
141
+ return 70;
142
+ }
143
+
144
+ // Mid-section pairs: compute max height of each row
145
+ const rowCount = Math.max(leftSections.length, rightSections.length);
146
+ let midHeight = 0;
147
+ for (let i = 0; i < rowCount; i++) {
148
+ const lh = leftSections[i] ? sectionHeight(leftSections[i]) : 0;
149
+ const rh = rightSections[i] ? sectionHeight(rightSections[i]) : 0;
150
+ midHeight += Math.max(lh, rh) + 8;
151
+ }
152
+
153
+ // Token footer
154
+ const tokenFooterH = show.tokens ? 44 : 0;
155
+ // Footer
156
+ const footerH = 36;
157
+
158
+ const naturalH = y + midHeight + tokenFooterH + footerH + 20;
159
+ // Stretch: if content is shorter than 675, distribute extra space into sections
160
+ const extraSpace = Math.max(0, H_FIXED - naturalH);
161
+ const extraPerRow = rowCount > 0 ? extraSpace / rowCount : 0;
51
162
 
52
- // Editor bar chart
163
+ // ── KPI cards ──
164
+ const kpiY = 94;
165
+ const kpiCards = [];
166
+ const kpiItems = [
167
+ { label: 'sessions', value: fmt(overview.totalChats) },
168
+ { label: 'tokens', value: fmt(totalTokens) },
169
+ { label: 'active days', value: String(streaks.totalDays || 0) },
170
+ { label: 'streak', value: `${streaks.current || 0}d` },
171
+ ];
172
+ if (show.costs && totalCost > 0) {
173
+ kpiItems.push({ label: 'est. cost', value: fmtCost(totalCost) });
174
+ }
175
+ const kpiW = (W - pad * 2 - (kpiItems.length - 1) * 8) / kpiItems.length;
176
+ kpiItems.forEach((item, i) => {
177
+ const x = pad + i * (kpiW + 8);
178
+ kpiCards.push(`
179
+ <rect x="${x}" y="${kpiY}" width="${kpiW}" height="64" rx="6" fill="${t.card}"/>
180
+ <text x="${x + 14}" y="${kpiY + 22}" fill="${t.text3}" font-size="11" font-family=${F}>${esc(item.label)}</text>
181
+ <text x="${x + 14}" y="${kpiY + 50}" fill="${t.text}" font-size="24" font-weight="bold" font-family=${F}>${esc(item.value)}</text>
182
+ `);
183
+ });
184
+
185
+ // ── Editor bar chart ──
53
186
  const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
54
- const editorBars = editors.slice(0, 8).map((e, i) => {
55
- const barW = Math.max((e.count / maxEditorCount) * 180, 4);
187
+ const editorBarsArr = editors.slice(0, 8).map((e, i) => {
188
+ const maxBarW = W / 2 - pad - 140;
189
+ const barW = Math.max((e.count / maxEditorCount) * maxBarW, 4);
56
190
  const color = EDITOR_COLORS[e.id] || '#6b7280';
57
- const label = (EDITOR_LABELS[e.id] || e.id).padEnd(12);
58
- const y = 170 + i * 22;
59
- return `
60
- <text x="30" y="${y + 12}" fill="#888" font-size="10" font-family="${F}">${esc(label)}</text>
61
- <rect x="140" y="${y + 1}" width="${barW}" height="14" fill="${color}" opacity="0.8"/>
62
- <text x="${146 + barW}" y="${y + 12}" fill="#aaa" font-size="9" font-family="${F}">${e.count}</text>
63
- `;
64
- }).join('');
65
-
66
- // Activity sparkline from hourly data
191
+ const label = (EDITOR_LABELS[e.id] || e.id);
192
+ return { label, barW, color, count: e.count };
193
+ });
194
+
195
+ // ── Cost bar chart ──
196
+ const maxCostVal = costByEditor.length > 0 ? Math.max(...costByEditor.map(c => c.cost), 0.01) : 1;
197
+ const costBarsArr = costByEditor.map(c => {
198
+ const maxBarW = W / 2 - pad - 140;
199
+ const barW = Math.max((c.cost / maxCostVal) * maxBarW, 4);
200
+ const color = EDITOR_COLORS[c.editor] || '#6b7280';
201
+ const label = EDITOR_LABELS[c.editor] || c.editor;
202
+ return { label, barW, color, value: fmtCost(c.cost) };
203
+ });
204
+
205
+ // ── Hourly sparkline ──
67
206
  const hourly = stats.hourly || new Array(24).fill(0);
68
- const maxH = Math.max(...hourly, 1);
69
- const sparkW = 180, sparkH = 40;
70
- const sparkPoints = hourly.map((v, i) => {
71
- const x = 590 + (i / 23) * sparkW;
72
- const y = 180 + sparkH - (v / maxH) * sparkH;
73
- return `${x},${y}`;
74
- }).join(' ');
75
-
76
- // Top models list
77
- const modelsList = topModels.map((m, i) => {
78
- const y = 274 + i * 16;
79
- const name = m.name.length > 24 ? m.name.substring(0, 24) : m.name;
80
- return `<text x="590" y="${y}" fill="#888" font-size="9" font-family="${F}">${esc(name)} <tspan fill="#555">${m.count}</tspan></text>`;
81
- }).join('');
82
-
83
- const dateStr = new Date().toISOString().split('T')[0];
84
-
85
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
86
- <!-- Background -->
87
- <rect width="${W}" height="${H}" fill="#000"/>
88
- <rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" fill="none" stroke="#222" stroke-width="1"/>
207
+ const maxHourly = Math.max(...hourly, 1);
89
208
 
90
- <!-- Terminal title bar -->
91
- <rect x="0" y="0" width="${W}" height="32" fill="#111"/>
92
- <text x="${W / 2}" y="20" fill="#555" font-size="11" font-family="${F}" text-anchor="middle">agentlytics</text>
209
+ // ── Build sections ──
210
+ let curY = kpiY + 64 + 18;
211
+ const sectionSvgs = [];
212
+ const colW = (W - pad * 2 - 20) / 2;
93
213
 
94
- <!-- Prompt line -->
95
- <text x="24" y="58" fill="#666" font-size="12" font-family="${F}">$</text>
96
- <text x="40" y="58" fill="#ccc" font-size="12" font-family="${F}">npx agentlytics</text>
214
+ for (let row = 0; row < rowCount; row++) {
215
+ const lName = leftSections[row];
216
+ const rName = rightSections[row];
217
+ const lh = lName ? sectionHeight(lName) : 0;
218
+ const rh = rName ? sectionHeight(rName) : 0;
219
+ const rowH = Math.max(lh, rh) + Math.round(extraPerRow);
97
220
 
98
- <!-- Divider -->
99
- <line x1="24" y1="68" x2="${W - 24}" y2="68" stroke="#222" stroke-width="1"/>
221
+ if (lName) sectionSvgs.push(renderSection(lName, pad, curY, colW, rowH));
222
+ if (rName) sectionSvgs.push(renderSection(rName, pad + colW + 20, curY, colW, rowH));
223
+
224
+ curY += rowH + 10;
225
+ }
226
+
227
+ function renderSection(name, sx, sy, sw, sh) {
228
+ let out = '';
229
+ if (name === 'editors') {
230
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># editors</text>`;
231
+ editorBarsArr.forEach((e, i) => {
232
+ const by = sy + 26 + i * 26;
233
+ out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(e.label)}</text>`;
234
+ out += `<rect x="${sx + 120}" y="${by + 1}" width="${e.barW}" height="16" rx="3" fill="${e.color}" opacity="0.85"/>`;
235
+ out += `<text x="${sx + 128 + e.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${e.count}</text>`;
236
+ });
237
+ } else if (name === 'costs') {
238
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># est. costs by editor</text>`;
239
+ costBarsArr.forEach((c, i) => {
240
+ const by = sy + 26 + i * 26;
241
+ out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(c.label)}</text>`;
242
+ out += `<rect x="${sx + 120}" y="${by + 1}" width="${c.barW}" height="16" rx="3" fill="${c.color}" opacity="0.85"/>`;
243
+ out += `<text x="${sx + 128 + c.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${c.value}</text>`;
244
+ });
245
+ } else if (name === 'hours') {
246
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># peak hours</text>`;
247
+ const sparkW = sw - 8;
248
+ const sparkH = 56;
249
+ const baseY = sy + 28;
250
+ const barW = sparkW / 24 - 1;
251
+ hourly.forEach((v, i) => {
252
+ const bh = Math.max((v / maxHourly) * sparkH, 1);
253
+ const bx = sx + i * (barW + 1);
254
+ const by = baseY + sparkH - bh;
255
+ const intensity = v / maxHourly;
256
+ const color = intensity > 0.75 ? t.hourHigh : intensity > 0.5 ? t.hourMed : intensity > 0.25 ? t.hourLow : t.hourMin;
257
+ out += `<rect x="${bx}" y="${by}" width="${barW}" height="${bh}" rx="1" fill="${color}" opacity="0.9"/>`;
258
+ });
259
+ out += `<text x="${sx}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>00</text>`;
260
+ out += `<text x="${sx + sparkW / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>06</text>`;
261
+ out += `<text x="${sx + sparkW / 2}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>12</text>`;
262
+ out += `<text x="${sx + sparkW * 3 / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>18</text>`;
263
+ out += `<text x="${sx + sparkW - 8}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>23</text>`;
264
+ } else if (name === 'models') {
265
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># top models</text>`;
266
+ topModels.forEach((m, i) => {
267
+ const my = sy + 32 + i * 22;
268
+ const mName = m.name.length > 28 ? m.name.substring(0, 28) + '…' : m.name;
269
+ out += `<text x="${sx}" y="${my}" fill="${t.text2}" font-size="11" font-family=${F}>${esc(mName)}</text>`;
270
+ out += `<text x="${sx + sw - 4}" y="${my}" fill="${t.text4}" font-size="11" font-family=${F} text-anchor="end">${m.count}</text>`;
271
+ });
272
+ }
273
+ return out;
274
+ }
100
275
 
101
- <!-- KPI row -->
102
- <rect x="24" y="78" width="175" height="58" fill="#111"/>
103
- <text x="36" y="96" fill="#666" font-size="9" font-family="${F}">sessions</text>
104
- <text x="36" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${fmt(overview.totalChats)}</text>
276
+ // ── Token footer line ──
277
+ let tokenLine = '';
278
+ if (show.tokens) {
279
+ const ty = curY + 4;
280
+ const tkPairs = [
281
+ ['input', fmt(tk.input)],
282
+ ['output', fmt(tk.output)],
283
+ ['cache read', fmt(tk.cacheRead)],
284
+ ['cache write', fmt(tk.cacheWrite)],
285
+ ['tools', fmt(stats.totalToolCalls || 0)],
286
+ ['editors', String(editors.length)],
287
+ ];
288
+ let tkX = pad;
289
+ let tkSvg = `<line x1="${pad}" y1="${ty}" x2="${W - pad}" y2="${ty}" stroke="${t.border}" stroke-width="1"/>`;
290
+ tkPairs.forEach(([label, val]) => {
291
+ tkSvg += `<text x="${tkX}" y="${ty + 20}" fill="${t.text5}" font-size="10" font-family=${F}>${label}</text>`;
292
+ tkSvg += `<text x="${tkX}" y="${ty + 34}" fill="${t.text2}" font-size="12" font-weight="600" font-family=${F}>${val}</text>`;
293
+ tkX += (W - pad * 2) / tkPairs.length;
294
+ });
295
+ tokenLine = tkSvg;
296
+ curY = ty + tokenFooterH;
297
+ }
105
298
 
106
- <rect x="210" y="78" width="175" height="58" fill="#111"/>
107
- <text x="222" y="96" fill="#666" font-size="9" font-family="${F}">tokens</text>
108
- <text x="222" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${fmt((tk.input || 0) + (tk.output || 0))}</text>
299
+ // ── Final footer pin to bottom of canvas ──
300
+ const footerY = H_FIXED - 28;
109
301
 
110
- <rect x="396" y="78" width="175" height="58" fill="#111"/>
111
- <text x="408" y="96" fill="#666" font-size="9" font-family="${F}">active_days</text>
112
- <text x="408" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${streaks.totalDays || 0}</text>
302
+ const usernameText = username ? `<text x="${W / 2}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="middle">${esc(username)}</text>` : '';
113
303
 
114
- <rect x="582" y="78" width="194" height="58" fill="#111"/>
115
- <text x="594" y="96" fill="#666" font-size="9" font-family="${F}">streak <tspan fill="#555">longest:${streaks.longest || 0}</tspan></text>
116
- <text x="594" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${streaks.current || 0} <tspan font-size="11" fill="#666">day${(streaks.current || 0) !== 1 ? 's' : ''}</tspan></text>
304
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 ${W} ${H_FIXED}">
305
+ <defs>
306
+ <linearGradient id="accentGrad" x1="0" y1="0" x2="1" y2="0">
307
+ <stop offset="0%" stop-color="#818cf8"/>
308
+ <stop offset="50%" stop-color="#a78bfa"/>
309
+ <stop offset="100%" stop-color="#c084fc"/>
310
+ </linearGradient>
311
+ </defs>
117
312
 
118
- <!-- Editors section -->
119
- <text x="24" y="160" fill="#666" font-size="10" font-family="${F}"># editors</text>
120
- ${editorBars}
313
+ <!-- Background -->
314
+ <rect width="${W}" height="${H_FIXED}" rx="12" fill="${t.bg}"/>
315
+ <rect x="0.5" y="0.5" width="${W - 1}" height="${H_FIXED - 1}" rx="12" fill="none" stroke="${t.border}" stroke-width="1"/>
316
+
317
+ <!-- Title bar -->
318
+ <rect x="1" y="1" width="${W - 2}" height="38" rx="12" fill="${t.bg2}"/>
319
+ <rect x="1" y="22" width="${W - 2}" height="17" fill="${t.bg2}"/>
320
+ <circle cx="20" cy="20" r="5" fill="#ef4444" opacity="0.7"/>
321
+ <circle cx="36" cy="20" r="5" fill="#f59e0b" opacity="0.7"/>
322
+ <circle cx="52" cy="20" r="5" fill="#22c55e" opacity="0.7"/>
323
+ <text x="${W / 2}" y="25" fill="${t.text2}" font-size="13" font-weight="600" font-family=${F} text-anchor="middle">agentlytics.io</text>
324
+
325
+ <!-- Accent line -->
326
+ <rect x="${pad}" y="44" width="60" height="2" rx="1" fill="url(#accentGrad)"/>
327
+
328
+ <!-- Branding -->
329
+ <g transform="translate(${pad}, 52) scale(0.8)" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
330
+ <path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
331
+ </g>
332
+ <text x="${pad + 26}" y="68" fill="${t.text}" font-size="16" font-weight="bold" font-family=${F}>Agentlytics</text>
333
+ <text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}>Your AI coding stats</text>
334
+ <text x="${W - pad}" y="68" fill="${t.text5}" font-size="12" font-family=${F} text-anchor="end">${esc(dateStr)}</text>
335
+
336
+ <!-- Divider -->
337
+ <line x1="${pad}" y1="78" x2="${W - pad}" y2="78" stroke="${t.border}" stroke-width="1"/>
121
338
 
122
- <!-- Right column: Peak Hours -->
123
- <text x="590" y="160" fill="#666" font-size="10" font-family="${F}"># peak_hours</text>
124
- <polyline points="${sparkPoints}" fill="none" stroke="#888" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
125
- <text x="590" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">00:00</text>
126
- <text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">23:00</text>
339
+ <!-- KPI cards -->
340
+ ${kpiCards.join('')}
127
341
 
128
- <!-- Top Models -->
129
- <text x="590" y="258" fill="#666" font-size="10" font-family="${F}"># models</text>
130
- ${modelsList}
342
+ <!-- Sections -->
343
+ ${sectionSvgs.join('')}
131
344
 
132
- <!-- Token breakdown -->
133
- <line x1="24" y1="${H - 62}" x2="${W - 24}" y2="${H - 62}" stroke="#222" stroke-width="1"/>
134
- <text x="24" y="${H - 44}" fill="#666" font-size="9" font-family="${F}">in:${fmt(tk.input)} out:${fmt(tk.output)} cache:${fmt(tk.cacheRead)} tools:${fmt(stats.totalToolCalls || 0)} editors:${editors.length}</text>
345
+ <!-- Token footer -->
346
+ ${tokenLine}
135
347
 
136
348
  <!-- Footer -->
137
- <line x1="24" y1="${H - 28}" x2="${W - 24}" y2="${H - 28}" stroke="#222" stroke-width="1"/>
138
- <text x="24" y="${H - 10}" fill="#555" font-size="9" font-family="${F}">github.com/f/agentlytics</text>
139
- <text x="${W - 24}" y="${H - 10}" fill="#555" font-size="9" font-family="${F}" text-anchor="end">${esc(dateStr)}</text>
349
+ <line x1="${pad}" y1="${footerY}" x2="${W - pad}" y2="${footerY}" stroke="${t.border}" stroke-width="1"/>
350
+ <text x="${pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F}>github.com/f/agentlytics</text>
351
+ ${usernameText}
352
+ <text x="${W - pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="end">npx agentlytics</text>
140
353
  </svg>`;
141
354
 
142
355
  return svg;
package/ui/src/App.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import { Routes, Route, NavLink } from 'react-router-dom'
3
- import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Radio, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
3
+ import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
4
4
  import { fetchOverview, refetchAgents, fetchMode, fetchRelayConfig, getAuthToken, setOnAuthFailure } from './lib/api'
5
5
  import { useTheme } from './lib/theme'
6
6
  import AnimatedLogo from './components/AnimatedLogo'
@@ -9,7 +9,6 @@ import Dashboard from './pages/Dashboard'
9
9
  import Sessions from './pages/Sessions'
10
10
  import DeepAnalysis from './pages/DeepAnalysis'
11
11
  import Compare from './pages/Compare'
12
- import ChatDetail from './pages/ChatDetail'
13
12
  import Projects from './pages/Projects'
14
13
  import ProjectDetail from './pages/ProjectDetail'
15
14
  import CostAnalysis from './pages/CostAnalysis'
@@ -4,15 +4,6 @@ import EditorDot from './EditorDot'
4
4
  import { editorLabel, formatNumber } from '../lib/constants'
5
5
  import { fetchRelayFeed } from '../lib/api'
6
6
 
7
- function timeAgo(ts) {
8
- if (!ts) return ''
9
- const diff = Date.now() - ts
10
- if (diff < 60000) return 'just now'
11
- if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
12
- if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
13
- return `${Math.floor(diff / 86400000)}d ago`
14
- }
15
-
16
7
  function timeLabel(ts) {
17
8
  if (!ts) return ''
18
9
  const d = new Date(ts)
@@ -22,7 +13,6 @@ function timeLabel(ts) {
22
13
  export default function LiveFeed({ onSessionClick }) {
23
14
  const [items, setItems] = useState([])
24
15
  const scrollRef = useRef(null)
25
- const prevCountRef = useRef(0)
26
16
 
27
17
  useEffect(() => {
28
18
  const load = () => {
@@ -146,22 +146,3 @@ export default function MessageContent({ content, toolCallDetails }) {
146
146
  })
147
147
  }
148
148
 
149
- /**
150
- * Renders a single message bubble with role icon, model tag, and content.
151
- */
152
- export function MessageBubble({ msg, toolCallDetails }) {
153
- const cfg = ROLE_CONFIG[msg.role] || ROLE_CONFIG.system
154
- const Icon = cfg.icon
155
- return (
156
- <div className="rounded-r-lg px-4 py-3" style={{ borderLeft: `2px solid ${cfg.borderColor}`, background: cfg.bg }}>
157
- <div className="flex items-center gap-2 text-xs mb-2" style={{ color: 'var(--c-text2)' }}>
158
- <Icon size={13} />
159
- <span className="font-medium">{cfg.label}</span>
160
- {msg.model && <span className="font-mono" style={{ color: 'var(--c-accent)', opacity: 0.6 }}>· {msg.model}</span>}
161
- </div>
162
- <div className="text-sm" style={{ color: 'var(--c-text)' }}>
163
- <MessageContent content={msg.content} toolCallDetails={toolCallDetails} />
164
- </div>
165
- </div>
166
- )
167
- }