agentlytics 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,19 +1,53 @@
1
- import { useState, useEffect, useRef } from 'react'
1
+ import { useState, useEffect, useRef, useMemo } from 'react'
2
2
  import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
3
3
  import { Doughnut, Bar } from 'react-chartjs-2'
4
- import { Loader2, X } from 'lucide-react'
4
+ import { Loader2, X, ArrowRight, Zap, MessageSquare, Wrench, Cpu, TrendingUp, BarChart3 } from 'lucide-react'
5
5
  import { fetchDeepAnalytics, fetchToolCalls } from '../lib/api'
6
6
  import { editorLabel, editorColor, formatNumber, formatDateTime, dateRangeToApiParams } from '../lib/constants'
7
7
  import { useTheme } from '../lib/theme'
8
8
  import KpiCard from '../components/KpiCard'
9
+ import EditorIcon from '../components/EditorIcon'
10
+ import SectionTitle from '../components/SectionTitle'
9
11
  import DateRangePicker from '../components/DateRangePicker'
10
12
 
11
13
  ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
12
14
 
13
15
  const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
16
+ const TOOL_COLORS = ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5', '#ecfdf5', '#b8f0d8', '#7ce0b8', '#4ade80', '#22c55e']
14
17
  const MONO = 'JetBrains Mono, monospace'
15
18
 
16
- // Summarize args for display — pick the most useful field
19
+ // Categorize tools into groups
20
+ const TOOL_CATEGORIES = {
21
+ 'File Operations': ['read_file', 'write_to_file', 'edit', 'multi_edit', 'Read', 'Write', 'EditFile', 'edit_file', 'create_file', 'read_notebook', 'edit_notebook'],
22
+ 'Search': ['grep_search', 'find_by_name', 'code_search', 'search', 'list_dir', 'Grep', 'Find', 'SearchFiles', 'ListDir'],
23
+ 'Terminal': ['run_command', 'command_status', 'RunCommand', 'execute_command', 'Bash', 'bash'],
24
+ 'Browser': ['browser_preview', 'read_url_content', 'view_content_chunk'],
25
+ 'AI Tools': ['mcp0_', 'mcp1_', 'mcp5_', 'mcp6_', 'skill', 'trajectory_search'],
26
+ }
27
+
28
+ function categorizeTools(tools) {
29
+ const cats = {}
30
+ for (const t of tools) {
31
+ let found = false
32
+ for (const [cat, patterns] of Object.entries(TOOL_CATEGORIES)) {
33
+ if (patterns.some(p => t.name.startsWith(p) || t.name === p || t.name.toLowerCase().includes(p.toLowerCase()))) {
34
+ if (!cats[cat]) cats[cat] = { tools: [], total: 0 }
35
+ cats[cat].tools.push(t)
36
+ cats[cat].total += t.count
37
+ found = true
38
+ break
39
+ }
40
+ }
41
+ if (!found) {
42
+ if (!cats['Other']) cats['Other'] = { tools: [], total: 0 }
43
+ cats['Other'].tools.push(t)
44
+ cats['Other'].total += t.count
45
+ }
46
+ }
47
+ return Object.entries(cats).sort((a, b) => b[1].total - a[1].total)
48
+ }
49
+
50
+ // Summarize args for display
17
51
  function summarizeArgs(toolName, args) {
18
52
  if (!args || typeof args !== 'object') return null
19
53
  if (args.CommandLine || args.command) return args.CommandLine || args.command
@@ -27,7 +61,6 @@ function summarizeArgs(toolName, args) {
27
61
  return vals.length > 0 ? vals[0].substring(0, 120) : null
28
62
  }
29
63
 
30
- // Detect if args contain a diff (old_string/new_string or similar)
31
64
  function getDiff(args) {
32
65
  if (!args || typeof args !== 'object') return null
33
66
  const old = args.old_string || args.old_text || args.oldText || args.search || null
@@ -76,7 +109,7 @@ function ToolCallRow({ call, toolName, index }) {
76
109
  return (
77
110
  <div className="px-2 py-1 text-[10px]" style={{ background: index % 2 === 0 ? 'var(--c-code-bg)' : 'transparent' }}>
78
111
  <div className="flex items-start gap-2 cursor-pointer" onClick={() => hasDetail && setExpanded(!expanded)}>
79
- <span className="w-2 h-2 mt-0.5 flex-shrink-0" style={{ background: editorColor(call.source) }} />
112
+ <EditorIcon source={call.source} size={10} />
80
113
  <div className="flex-1 min-w-0">
81
114
  {summary && (
82
115
  <div className="font-mono truncate" style={{ color: 'var(--c-white)' }} title={summary}>{summary}</div>
@@ -85,7 +118,7 @@ function ToolCallRow({ call, toolName, index }) {
85
118
  <span>{editorLabel(call.source)}</span>
86
119
  {project && <span>· {project}</span>}
87
120
  {call.timestamp && <span>· {new Date(call.timestamp).toLocaleDateString()}</span>}
88
- {hasDetail && <span>{expanded ? '[-]' : '[+]'}</span>}
121
+ {hasDetail && <span>{expanded ? '' : ''}</span>}
89
122
  </div>
90
123
  </div>
91
124
  </div>
@@ -113,10 +146,11 @@ function ToolDrillDown({ toolName, folder, onClose }) {
113
146
  <div className="card p-3 fade-in" style={{ borderColor: 'rgba(99,102,241,0.3)' }}>
114
147
  <div className="flex items-center justify-between mb-2">
115
148
  <div className="flex items-center gap-2">
149
+ <Wrench size={12} style={{ color: 'var(--c-accent)' }} />
116
150
  <span className="text-xs font-bold" style={{ color: 'var(--c-white)' }}>{toolName}</span>
117
151
  <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>
118
152
  {calls ? `${calls.length} calls` : '...'}
119
- {calls && projectName ? ` for ${projectName}` : ''}
153
+ {calls && projectName ? ` in ${projectName}` : ''}
120
154
  </span>
121
155
  </div>
122
156
  <button onClick={onClose} className="p-0.5" style={{ color: 'var(--c-text2)' }}><X size={12} /></button>
@@ -136,6 +170,24 @@ function ToolDrillDown({ toolName, folder, onClose }) {
136
170
  )
137
171
  }
138
172
 
173
+ // Proportional bar component
174
+ function ProportionBar({ segments, height = 6 }) {
175
+ const total = segments.reduce((s, seg) => s + seg.value, 0)
176
+ if (total === 0) return null
177
+ return (
178
+ <div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
179
+ {segments.filter(s => s.value > 0).map((seg, i) => (
180
+ <div
181
+ key={i}
182
+ title={`${seg.label}: ${formatNumber(seg.value)}`}
183
+ className="h-full transition-all"
184
+ style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
185
+ />
186
+ ))}
187
+ </div>
188
+ )
189
+ }
190
+
139
191
  export default function DeepAnalysis({ overview }) {
140
192
  const [editor, setEditor] = useState('')
141
193
  const [folder, setFolder] = useState('')
@@ -165,6 +217,21 @@ export default function DeepAnalysis({ overview }) {
165
217
  const tools = data?.topTools?.slice(0, 15) || []
166
218
  const models = data?.topModels?.slice(0, 10) || []
167
219
 
220
+ // Computed insights
221
+ const insights = useMemo(() => {
222
+ if (!data) return null
223
+ const totalTok = data.totalInputTokens + data.totalOutputTokens
224
+ const msgsPerSession = data.analyzedChats > 0 ? (data.totalMessages / data.analyzedChats).toFixed(1) : 0
225
+ const toolsPerSession = data.analyzedChats > 0 ? (data.totalToolCalls / data.analyzedChats).toFixed(1) : 0
226
+ const tokPerMsg = data.totalMessages > 0 ? Math.round(totalTok / data.totalMessages) : 0
227
+ const cacheHitRate = data.totalInputTokens > 0 ? ((data.totalCacheRead / data.totalInputTokens) * 100).toFixed(1) : 0
228
+ const outputRatio = data.totalInputTokens > 0 ? (data.totalOutputTokens / data.totalInputTokens).toFixed(2) : 0
229
+ const aiVsHuman = data.totalUserChars > 0 ? (data.totalAssistantChars / data.totalUserChars).toFixed(1) : 0
230
+ return { totalTok, msgsPerSession, toolsPerSession, tokPerMsg, cacheHitRate, outputRatio, aiVsHuman }
231
+ }, [data])
232
+
233
+ const toolCategories = useMemo(() => tools.length > 0 ? categorizeTools(tools) : [], [tools])
234
+
168
235
  function handleToolClick(evt, elements) {
169
236
  if (elements.length > 0) {
170
237
  const idx = elements[0].index
@@ -174,12 +241,13 @@ export default function DeepAnalysis({ overview }) {
174
241
  }
175
242
 
176
243
  return (
177
- <div className="fade-in space-y-3">
244
+ <div className="fade-in space-y-4">
245
+ {/* Filters */}
178
246
  <div className="flex items-center gap-2">
179
247
  <select
180
248
  value={editor}
181
249
  onChange={e => setEditor(e.target.value)}
182
- className="px-2 py-1 text-[11px] outline-none"
250
+ className="px-2 py-1.5 text-[11px] outline-none rounded-sm"
183
251
  style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
184
252
  >
185
253
  <option value="">All Editors</option>
@@ -190,7 +258,7 @@ export default function DeepAnalysis({ overview }) {
190
258
  <select
191
259
  value={folder}
192
260
  onChange={e => setFolder(e.target.value)}
193
- className="px-2 py-1 text-[11px] outline-none max-w-[200px] truncate"
261
+ className="px-2 py-1.5 text-[11px] outline-none max-w-[200px] truncate rounded-sm"
194
262
  style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
195
263
  >
196
264
  <option value="">All Projects</option>
@@ -198,35 +266,151 @@ export default function DeepAnalysis({ overview }) {
198
266
  <option key={p.fullPath || p.name} value={p.fullPath}>{p.name}</option>
199
267
  ))}
200
268
  </select>
201
- {loading && (
202
- <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />
203
- )}
204
- {data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions</span>}
269
+ {loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
270
+ {data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions analyzed</span>}
205
271
  <div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
206
272
  </div>
207
273
 
208
- {data && (
274
+ {data && insights && (
209
275
  <>
210
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-2">
276
+ {/* KPIs */}
277
+ <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
211
278
  <KpiCard label="sessions" value={data.analyzedChats} />
212
- <KpiCard label="messages" value={formatNumber(data.totalMessages)} />
213
- <KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} />
214
- <KpiCard label="input tokens" value={formatNumber(data.totalInputTokens)} sub={data.totalCacheRead > 0 ? `${formatNumber(data.totalCacheRead)} cached` : undefined} />
215
- <KpiCard label="output tokens" value={formatNumber(data.totalOutputTokens)} />
279
+ <KpiCard label="messages" value={formatNumber(data.totalMessages)} sub={`${insights.msgsPerSession}/session`} />
280
+ <KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} sub={`${insights.toolsPerSession}/session`} />
281
+ <KpiCard label="total tokens" value={formatNumber(insights.totalTok)} sub={`${formatNumber(insights.tokPerMsg)}/msg`} />
282
+ <KpiCard label="you wrote" value={formatNumber(data.totalUserChars)} sub={`AI: ${insights.aiVsHuman}× more`} />
216
283
  </div>
217
284
 
218
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
285
+ {/* Token flow + Insights row */}
286
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
287
+ {/* Token flow visualization */}
288
+ <div className="card p-3">
289
+ <SectionTitle>token flow</SectionTitle>
290
+ <div className="space-y-3 mt-2">
291
+ {/* Input tokens breakdown */}
292
+ <div>
293
+ <div className="flex items-center justify-between text-[10px] mb-1">
294
+ <span style={{ color: 'var(--c-text2)' }}>input tokens</span>
295
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens)}</span>
296
+ </div>
297
+ <ProportionBar segments={[
298
+ { label: 'Fresh input', value: data.totalInputTokens - data.totalCacheRead, color: '#6366f1' },
299
+ { label: 'Cache read', value: data.totalCacheRead, color: '#34d399' },
300
+ ]} />
301
+ <div className="flex items-center gap-3 mt-1 text-[9px]">
302
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens - data.totalCacheRead)}</span>
303
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cached {formatNumber(data.totalCacheRead)}</span>
304
+ </div>
305
+ </div>
306
+ {/* Output tokens */}
307
+ <div>
308
+ <div className="flex items-center justify-between text-[10px] mb-1">
309
+ <span style={{ color: 'var(--c-text2)' }}>output tokens</span>
310
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalOutputTokens)}</span>
311
+ </div>
312
+ <ProportionBar segments={[
313
+ { label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
314
+ ]} />
315
+ </div>
316
+ {/* Cache write */}
317
+ {data.totalCacheWrite > 0 && (
318
+ <div>
319
+ <div className="flex items-center justify-between text-[10px] mb-1">
320
+ <span style={{ color: 'var(--c-text2)' }}>cache write</span>
321
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalCacheWrite)}</span>
322
+ </div>
323
+ <ProportionBar segments={[
324
+ { label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
325
+ ]} />
326
+ </div>
327
+ )}
328
+ {/* Overall ratio bar */}
329
+ <div className="pt-2" style={{ borderTop: '1px solid var(--c-border)' }}>
330
+ <div className="text-[9px] mb-1" style={{ color: 'var(--c-text3)' }}>overall token distribution</div>
331
+ <ProportionBar height={10} segments={[
332
+ { label: 'Input', value: data.totalInputTokens, color: '#6366f1' },
333
+ { label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
334
+ { label: 'Cache Read', value: data.totalCacheRead, color: '#34d399' },
335
+ { label: 'Cache Write', value: data.totalCacheWrite, color: '#fbbf24' },
336
+ ]} />
337
+ <div className="flex items-center gap-3 mt-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
338
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> in</span>
339
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> out</span>
340
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read</span>
341
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write</span>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ {/* Efficiency insights */}
219
348
  <div className="card p-3">
220
- <h3 className="text-[10px] font-medium uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>
221
- most used tools <span style={{ color: 'var(--c-text3)' }}>(click to drill down)</span>
222
- </h3>
223
- <div style={{ height: tools.length * 22 + 20, minHeight: 120 }}>
349
+ <SectionTitle>efficiency insights</SectionTitle>
350
+ <div className="space-y-3 mt-2">
351
+ {/* Human vs AI chars */}
352
+ <div>
353
+ <div className="text-[10px] mb-1" style={{ color: 'var(--c-text2)' }}>you vs AI (characters)</div>
354
+ <ProportionBar height={8} segments={[
355
+ { label: 'You', value: data.totalUserChars, color: '#6366f1' },
356
+ { label: 'AI', value: data.totalAssistantChars, color: '#34d399' },
357
+ ]} />
358
+ <div className="flex items-center justify-between mt-1 text-[9px]">
359
+ <span style={{ color: '#6366f1' }}>You: {formatNumber(data.totalUserChars)}</span>
360
+ <span style={{ color: '#34d399' }}>AI: {formatNumber(data.totalAssistantChars)}</span>
361
+ </div>
362
+ </div>
363
+
364
+ {/* Metric cards */}
365
+ <div className="grid grid-cols-2 gap-2">
366
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
367
+ <div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
368
+ <TrendingUp size={9} /> output/input ratio
369
+ </div>
370
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.outputRatio}×</div>
371
+ </div>
372
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
373
+ <div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
374
+ <Zap size={9} /> cache hit rate
375
+ </div>
376
+ <div className="text-sm font-bold mt-0.5" style={{ color: parseFloat(insights.cacheHitRate) > 50 ? '#34d399' : parseFloat(insights.cacheHitRate) > 20 ? '#fbbf24' : 'var(--c-white)' }}>{insights.cacheHitRate}%</div>
377
+ </div>
378
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
379
+ <div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
380
+ <MessageSquare size={9} /> tokens per message
381
+ </div>
382
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{formatNumber(insights.tokPerMsg)}</div>
383
+ </div>
384
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
385
+ <div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
386
+ <Wrench size={9} /> tools per session
387
+ </div>
388
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.toolsPerSession}</div>
389
+ </div>
390
+ </div>
391
+
392
+ {/* AI amplification */}
393
+ <div className="p-2 rounded-sm text-center" style={{ background: 'var(--c-code-bg)' }}>
394
+ <div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>AI amplification factor</div>
395
+ <div className="text-lg font-bold" style={{ color: 'var(--c-accent)' }}>{insights.aiVsHuman}×</div>
396
+ <div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>AI writes {insights.aiVsHuman}× more than you type</div>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ {/* Charts row: Tools + Models + Tool Categories */}
403
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
404
+ {/* Tools bar */}
405
+ <div className="card p-3 lg:col-span-2">
406
+ <SectionTitle>most used tools <span style={{ color: 'var(--c-text3)' }}>(click to drill down)</span></SectionTitle>
407
+ <div style={{ height: Math.max(tools.length * 20 + 10, 120) }}>
224
408
  {tools.length > 0 ? (
225
409
  <Bar
226
410
  ref={chartRef}
227
411
  data={{
228
412
  labels: tools.map(t => t.name),
229
- datasets: [{ data: tools.map(t => t.count), backgroundColor: '#6366f1' }],
413
+ datasets: [{ data: tools.map(t => t.count), backgroundColor: TOOL_COLORS.concat(MODEL_COLORS).slice(0, tools.length), borderRadius: 2 }],
230
414
  }}
231
415
  options={{
232
416
  indexAxis: 'y',
@@ -234,20 +418,20 @@ export default function DeepAnalysis({ overview }) {
234
418
  maintainAspectRatio: false,
235
419
  onClick: handleToolClick,
236
420
  scales: {
237
- x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 9, family: MONO } } },
238
- y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 9, family: MONO } } },
421
+ x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
422
+ y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
239
423
  },
240
- plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 } } },
424
+ plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
241
425
  }}
242
426
  />
243
- ) : (
244
- <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>
245
- )}
427
+ ) : <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>}
246
428
  </div>
247
429
  </div>
430
+
431
+ {/* Models doughnut */}
248
432
  <div className="card p-3">
249
- <h3 className="text-[10px] font-medium uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>models</h3>
250
- <div style={{ height: 240 }}>
433
+ <SectionTitle>models</SectionTitle>
434
+ <div style={{ height: 160 }}>
251
435
  {models.length > 0 ? (
252
436
  <Doughnut
253
437
  data={{
@@ -255,21 +439,31 @@ export default function DeepAnalysis({ overview }) {
255
439
  datasets: [{ data: models.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
256
440
  }}
257
441
  options={{
258
- responsive: true,
259
- maintainAspectRatio: false,
260
- cutout: '55%',
442
+ responsive: true, maintainAspectRatio: false, cutout: '60%',
261
443
  plugins: {
262
- legend: {
263
- position: 'right',
264
- labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 },
265
- },
444
+ legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
445
+ tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
266
446
  },
267
447
  }}
268
448
  />
269
- ) : (
270
- <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>
271
- )}
449
+ ) : <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>}
272
450
  </div>
451
+
452
+ {/* Tool categories below models */}
453
+ {toolCategories.length > 0 && (
454
+ <div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--c-border)' }}>
455
+ <div className="text-[9px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text3)' }}>tool categories</div>
456
+ <div className="space-y-1.5">
457
+ {toolCategories.map(([cat, { tools: catTools, total }]) => (
458
+ <div key={cat} className="flex items-center gap-2 text-[10px]">
459
+ <span className="truncate flex-1" style={{ color: 'var(--c-text2)' }}>{cat}</span>
460
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{total}</span>
461
+ <span style={{ color: 'var(--c-text3)' }}>({catTools.length})</span>
462
+ </div>
463
+ ))}
464
+ </div>
465
+ </div>
466
+ )}
273
467
  </div>
274
468
  </div>
275
469
 
@@ -277,26 +471,6 @@ export default function DeepAnalysis({ overview }) {
277
471
  {selectedTool && (
278
472
  <ToolDrillDown toolName={selectedTool} folder={folder} onClose={() => setSelectedTool(null)} />
279
473
  )}
280
-
281
- {/* Token breakdown */}
282
- {(data.totalInputTokens > 0 || data.totalOutputTokens > 0) && (
283
- <div className="card p-3">
284
- <h3 className="text-[10px] font-medium uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>tokens</h3>
285
- <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
286
- {[
287
- ['Input', data.totalInputTokens],
288
- ['Output', data.totalOutputTokens],
289
- ['Cache Read', data.totalCacheRead],
290
- ['Cache Write', data.totalCacheWrite],
291
- ].map(([label, val]) => (
292
- <div key={label}>
293
- <div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>{label}</div>
294
- <div className="text-sm font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(val)}</div>
295
- </div>
296
- ))}
297
- </div>
298
- </div>
299
- )}
300
474
  </>
301
475
  )}
302
476
  </div>