memhouse 0.0.1 → 0.3.1

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 (97) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +72 -3
  3. package/bin/memhouse.js +559 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/adapter-errors.js +52 -0
  9. package/editors/antigravity.js +1048 -0
  10. package/editors/base.js +321 -0
  11. package/editors/claude.js +386 -0
  12. package/editors/codebuff.js +338 -0
  13. package/editors/codex.js +537 -0
  14. package/editors/commandcode.js +166 -0
  15. package/editors/copilot-jetbrains.js +207 -0
  16. package/editors/copilot.js +260 -0
  17. package/editors/cursor-agent.js +202 -0
  18. package/editors/cursor.js +498 -0
  19. package/editors/gemini.js +231 -0
  20. package/editors/goose.js +386 -0
  21. package/editors/gsd.js +366 -0
  22. package/editors/index.js +274 -0
  23. package/editors/kiro.js +315 -0
  24. package/editors/opencode.js +432 -0
  25. package/editors/vscode.js +413 -0
  26. package/editors/windsurf.js +634 -0
  27. package/editors/zed.js +229 -0
  28. package/index.js +424 -2
  29. package/mcp-server.js +294 -0
  30. package/mem-house/DESIGN.md +119 -0
  31. package/mem-house/delivery/AGENT-INSTALL.md +87 -0
  32. package/mem-house/delivery/PROMPT.md +42 -0
  33. package/mem-house/delivery/install.sh +181 -0
  34. package/mem-house/delivery/kernel-install.md +83 -0
  35. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  36. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  37. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  38. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  39. package/mem-house/rls.sql +17 -0
  40. package/mem-house/schema.sql +145 -0
  41. package/mem-house/server/queries.js +878 -0
  42. package/mem-house/server/server.js +228 -0
  43. package/mem-house/shipper/ship.js +461 -0
  44. package/misc/syntax-check.js +38 -0
  45. package/package.json +59 -7
  46. package/pricing.js +88 -0
  47. package/pricing.json +992 -0
  48. package/public/assets/index-CCPh7a2Z.js +75 -0
  49. package/public/assets/index-DV6ONi_F.css +2 -0
  50. package/public/index.html +16 -0
  51. package/relay-client.js +306 -0
  52. package/relay-server.js +577 -0
  53. package/server.js +860 -0
  54. package/share-image.js +367 -0
  55. package/ui/eslint.config.js +29 -0
  56. package/ui/index.html +15 -0
  57. package/ui/package-lock.json +4229 -0
  58. package/ui/package.json +38 -0
  59. package/ui/src/App.jsx +364 -0
  60. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  61. package/ui/src/components/AiAuditCard.jsx +254 -0
  62. package/ui/src/components/AnimatedLoader.jsx +14 -0
  63. package/ui/src/components/AnimatedLogo.jsx +96 -0
  64. package/ui/src/components/ChatSidebar.jsx +226 -0
  65. package/ui/src/components/DateRangePicker.jsx +104 -0
  66. package/ui/src/components/EditorDot.jsx +11 -0
  67. package/ui/src/components/EditorIcon.jsx +117 -0
  68. package/ui/src/components/KpiCard.jsx +9 -0
  69. package/ui/src/components/LiveFeed.jsx +128 -0
  70. package/ui/src/components/LoginScreen.jsx +81 -0
  71. package/ui/src/components/MessageRenderer.jsx +148 -0
  72. package/ui/src/components/PageHeader.jsx +11 -0
  73. package/ui/src/components/SectionTitle.jsx +3 -0
  74. package/ui/src/components/ShareModal.jsx +305 -0
  75. package/ui/src/components/TokenTimeline.jsx +258 -0
  76. package/ui/src/hooks/useLive.jsx +15 -0
  77. package/ui/src/index.css +83 -0
  78. package/ui/src/lib/api.js +343 -0
  79. package/ui/src/lib/constants.js +88 -0
  80. package/ui/src/lib/theme.jsx +23 -0
  81. package/ui/src/main.jsx +16 -0
  82. package/ui/src/pages/Artifacts.jsx +575 -0
  83. package/ui/src/pages/Compare.jsx +232 -0
  84. package/ui/src/pages/CostAnalysis.jsx +354 -0
  85. package/ui/src/pages/Dashboard.jsx +529 -0
  86. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  87. package/ui/src/pages/GSD.jsx +726 -0
  88. package/ui/src/pages/MCPs.jsx +744 -0
  89. package/ui/src/pages/ProjectDetail.jsx +325 -0
  90. package/ui/src/pages/Projects.jsx +249 -0
  91. package/ui/src/pages/RelayDashboard.jsx +590 -0
  92. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  93. package/ui/src/pages/Sessions.jsx +535 -0
  94. package/ui/src/pages/Settings.jsx +241 -0
  95. package/ui/src/pages/SqlViewer.jsx +370 -0
  96. package/ui/src/pages/Subscriptions.jsx +411 -0
  97. package/ui/vite.config.js +18 -0
@@ -0,0 +1,495 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react'
2
+ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
3
+ import { Doughnut, Bar } from 'react-chartjs-2'
4
+ import { Loader2, X, Zap, MessageSquare, Wrench, Cpu, TrendingUp } from 'lucide-react'
5
+ import { fetchDeepAnalytics, fetchToolCalls, fetchCosts } from '../lib/api'
6
+ import { editorLabel, editorColor, formatNumber, formatCost, dateRangeToApiParams } from '../lib/constants'
7
+ import { useTheme } from '../lib/theme'
8
+ import KpiCard from '../components/KpiCard'
9
+ import EditorIcon from '../components/EditorIcon'
10
+ import SectionTitle from '../components/SectionTitle'
11
+ import DateRangePicker from '../components/DateRangePicker'
12
+ import PageHeader from '../components/PageHeader'
13
+
14
+ ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
15
+
16
+ const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
17
+ const TOOL_COLORS = ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5', '#ecfdf5', '#b8f0d8', '#7ce0b8', '#4ade80', '#22c55e']
18
+ const MONO = 'JetBrains Mono, monospace'
19
+
20
+ // Categorize tools into groups
21
+ const TOOL_CATEGORIES = {
22
+ 'File Operations': ['read_file', 'write_to_file', 'edit', 'multi_edit', 'Read', 'Write', 'EditFile', 'edit_file', 'create_file', 'read_notebook', 'edit_notebook'],
23
+ 'Search': ['grep_search', 'find_by_name', 'code_search', 'search', 'list_dir', 'Grep', 'Find', 'SearchFiles', 'ListDir'],
24
+ 'Terminal': ['run_command', 'command_status', 'RunCommand', 'execute_command', 'Bash', 'bash'],
25
+ 'Browser': ['browser_preview', 'read_url_content', 'view_content_chunk'],
26
+ 'AI Tools': ['mcp0_', 'mcp1_', 'mcp5_', 'mcp6_', 'skill', 'trajectory_search'],
27
+ }
28
+
29
+ function categorizeTools(tools) {
30
+ const cats = {}
31
+ for (const t of tools) {
32
+ let found = false
33
+ for (const [cat, patterns] of Object.entries(TOOL_CATEGORIES)) {
34
+ if (patterns.some(p => t.name.startsWith(p) || t.name === p || t.name.toLowerCase().includes(p.toLowerCase()))) {
35
+ if (!cats[cat]) cats[cat] = { tools: [], total: 0 }
36
+ cats[cat].tools.push(t)
37
+ cats[cat].total += t.count
38
+ found = true
39
+ break
40
+ }
41
+ }
42
+ if (!found) {
43
+ if (!cats['Other']) cats['Other'] = { tools: [], total: 0 }
44
+ cats['Other'].tools.push(t)
45
+ cats['Other'].total += t.count
46
+ }
47
+ }
48
+ return Object.entries(cats).sort((a, b) => b[1].total - a[1].total)
49
+ }
50
+
51
+ // Summarize args for display
52
+ function summarizeArgs(toolName, args) {
53
+ if (!args || typeof args !== 'object') return null
54
+ if (args.CommandLine || args.command) return args.CommandLine || args.command
55
+ if (args.file_path) return args.file_path
56
+ if (args.TargetFile) return args.TargetFile
57
+ if (args.SearchPath || args.search_path) return `${args.Query || args.query || ''} in ${args.SearchPath || args.search_path || ''}`
58
+ if (args.Pattern) return `${args.Pattern} in ${args.SearchDirectory || ''}`
59
+ if (args.Url || args.url) return args.Url || args.url
60
+ if (args.query) return args.query
61
+ const vals = Object.values(args).filter(v => typeof v === 'string' && v.length > 0)
62
+ return vals.length > 0 ? vals[0].substring(0, 120) : null
63
+ }
64
+
65
+ function getDiff(args) {
66
+ if (!args || typeof args !== 'object') return null
67
+ const old = args.old_string || args.old_text || args.oldText || args.search || null
68
+ const nw = args.new_string || args.new_text || args.newText || args.replace || null
69
+ const file = args.file_path || args.TargetFile || args.filePath || args.path || null
70
+ if (old != null || nw != null) return { old, new: nw, file }
71
+ return null
72
+ }
73
+
74
+ function DiffBlock({ diff }) {
75
+ const maxLines = 8
76
+ const oldLines = (diff.old || '').split('\n').slice(0, maxLines)
77
+ const newLines = (diff.new || '').split('\n').slice(0, maxLines)
78
+ return (
79
+ <div className="mt-1 text-[10px] font-mono overflow-x-auto" style={{ border: '1px solid var(--c-border)' }}>
80
+ {diff.file && (
81
+ <div className="px-2 py-0.5" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>{diff.file}</div>
82
+ )}
83
+ {diff.old && oldLines.map((line, i) => (
84
+ <div key={'o' + i} className="px-2" style={{ background: 'rgba(248,113,113,0.07)', color: '#f87171' }}>
85
+ <span style={{ color: 'var(--c-text3)', userSelect: 'none' }}>- </span>{line}
86
+ </div>
87
+ ))}
88
+ {diff.old && oldLines.length < (diff.old || '').split('\n').length && (
89
+ <div className="px-2" style={{ color: 'var(--c-text3)' }}> ... {(diff.old || '').split('\n').length - maxLines} more lines</div>
90
+ )}
91
+ {diff.new && newLines.map((line, i) => (
92
+ <div key={'n' + i} className="px-2" style={{ background: 'rgba(52,211,153,0.07)', color: '#34d399' }}>
93
+ <span style={{ color: 'var(--c-text3)', userSelect: 'none' }}>+ </span>{line}
94
+ </div>
95
+ ))}
96
+ {diff.new && newLines.length < (diff.new || '').split('\n').length && (
97
+ <div className="px-2" style={{ color: 'var(--c-text3)' }}> ... {(diff.new || '').split('\n').length - maxLines} more lines</div>
98
+ )}
99
+ </div>
100
+ )
101
+ }
102
+
103
+ function ToolCallRow({ call, toolName, index }) {
104
+ const [expanded, setExpanded] = useState(false)
105
+ const summary = summarizeArgs(toolName, call.args)
106
+ const diff = getDiff(call.args)
107
+ const project = call.folder ? call.folder.split('/').pop() : null
108
+ const hasDetail = diff || (call.args && Object.keys(call.args).length > 0)
109
+
110
+ return (
111
+ <div className="px-2 py-1 text-[11px]" style={{ background: index % 2 === 0 ? 'var(--c-code-bg)' : 'transparent' }}>
112
+ <div className="flex items-start gap-2 cursor-pointer" onClick={() => hasDetail && setExpanded(!expanded)}>
113
+ <EditorIcon source={call.source} size={10} />
114
+ <div className="flex-1 min-w-0">
115
+ {summary && (
116
+ <div className="font-mono truncate" style={{ color: 'var(--c-white)' }} title={summary}>{summary}</div>
117
+ )}
118
+ <div className="flex items-center gap-2" style={{ color: 'var(--c-text3)' }}>
119
+ <span>{editorLabel(call.source)}</span>
120
+ {project && <span>· {project}</span>}
121
+ {call.timestamp && <span>· {new Date(call.timestamp).toLocaleDateString()}</span>}
122
+ {hasDetail && <span>{expanded ? '▾' : '▸'}</span>}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ {expanded && diff && <DiffBlock diff={diff} />}
127
+ {expanded && !diff && call.args && Object.keys(call.args).length > 0 && (
128
+ <pre className="mt-1 px-2 py-1 text-[10px] overflow-x-auto whitespace-pre-wrap break-all" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
129
+ {JSON.stringify(call.args, null, 2)}
130
+ </pre>
131
+ )}
132
+ </div>
133
+ )
134
+ }
135
+
136
+ function ToolDrillDown({ toolName, folder, editor, dateRange, onClose }) {
137
+ const [calls, setCalls] = useState(null)
138
+ const [loading, setLoading] = useState(true)
139
+ const projectName = folder ? folder.split('/').pop() : null
140
+
141
+ useEffect(() => {
142
+ setLoading(true)
143
+ // Same filters as the charts this drill-down was opened from — otherwise the
144
+ // call list can show other editors/dates than the clicked segment counted.
145
+ fetchToolCalls(toolName, {
146
+ folder: folder || undefined,
147
+ editor: editor || undefined,
148
+ ...dateRangeToApiParams(dateRange),
149
+ }).then(data => { setCalls(data); setLoading(false) })
150
+ }, [toolName, folder, editor, dateRange])
151
+
152
+ return (
153
+ <div className="card p-3 fade-in" style={{ borderColor: 'rgba(99,102,241,0.3)' }}>
154
+ <div className="flex items-center justify-between mb-2">
155
+ <div className="flex items-center gap-2">
156
+ <Wrench size={12} style={{ color: 'var(--c-accent)' }} />
157
+ <span className="text-xs font-bold" style={{ color: 'var(--c-white)' }}>{toolName}</span>
158
+ <span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>
159
+ {calls ? `${calls.length} calls` : '...'}
160
+ {calls && projectName ? ` in ${projectName}` : ''}
161
+ </span>
162
+ </div>
163
+ <button onClick={onClose} className="p-0.5" style={{ color: 'var(--c-text2)' }}><X size={12} /></button>
164
+ </div>
165
+ {loading ? (
166
+ <div className="text-[11px] py-4 text-center" style={{ color: 'var(--c-text3)' }}>loading...</div>
167
+ ) : calls && calls.length > 0 ? (
168
+ <div className="max-h-[500px] overflow-y-auto scrollbar-thin space-y-0.5">
169
+ {calls.map((c, i) => (
170
+ <ToolCallRow key={i} call={c} toolName={toolName} index={i} />
171
+ ))}
172
+ </div>
173
+ ) : (
174
+ <div className="text-[11px] py-4 text-center" style={{ color: 'var(--c-text3)' }}>no calls found</div>
175
+ )}
176
+ </div>
177
+ )
178
+ }
179
+
180
+ // Proportional bar component
181
+ function ProportionBar({ segments, height = 6 }) {
182
+ const total = segments.reduce((s, seg) => s + seg.value, 0)
183
+ if (total === 0) return null
184
+ return (
185
+ <div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
186
+ {segments.filter(s => s.value > 0).map((seg, i) => (
187
+ <div
188
+ key={i}
189
+ title={`${seg.label}: ${formatNumber(seg.value)}`}
190
+ className="h-full transition-all"
191
+ style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
192
+ />
193
+ ))}
194
+ </div>
195
+ )
196
+ }
197
+
198
+ export default function DeepAnalysis({ overview }) {
199
+ const [editor, setEditor] = useState('')
200
+ const [folder, setFolder] = useState('')
201
+ const [dateRange, setDateRange] = useState(null)
202
+ const [data, setData] = useState(null)
203
+ const [loading, setLoading] = useState(false)
204
+ const [selectedTool, setSelectedTool] = useState(null)
205
+ const [costs, setCosts] = useState(null)
206
+ const chartRef = useRef(null)
207
+ const { dark } = useTheme()
208
+ const txtColor = dark ? '#a0a0a0' : '#444'
209
+ const txtDim = dark ? '#555' : '#999'
210
+ const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
211
+ const legendColor = dark ? '#9ca3af' : '#555'
212
+
213
+ const editors = overview?.editors || []
214
+ const projects = overview?.topProjects || []
215
+
216
+ async function analyze() {
217
+ setLoading(true)
218
+ const dateParams = dateRangeToApiParams(dateRange)
219
+ const [result, costData] = await Promise.all([
220
+ fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500, ...dateParams }),
221
+ fetchCosts({ editor, folder: folder || undefined, ...dateParams }),
222
+ ])
223
+ setData(result)
224
+ setCosts(costData)
225
+ setLoading(false)
226
+ }
227
+
228
+ useEffect(() => { analyze() }, [editor, folder, dateRange])
229
+
230
+ const tools = data?.topTools?.slice(0, 15) || []
231
+ const models = data?.topModels?.slice(0, 10) || []
232
+
233
+ // Computed insights
234
+ const insights = useMemo(() => {
235
+ if (!data) return null
236
+ const totalTok = data.totalInputTokens + data.totalOutputTokens + data.totalCacheRead + data.totalCacheWrite
237
+ const msgsPerSession = data.analyzedChats > 0 ? (data.totalMessages / data.analyzedChats).toFixed(1) : 0
238
+ const toolsPerSession = data.analyzedChats > 0 ? (data.totalToolCalls / data.analyzedChats).toFixed(1) : 0
239
+ const tokPerMsg = data.totalMessages > 0 ? Math.round(totalTok / data.totalMessages) : 0
240
+ const totalInputAll = data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite
241
+ const cacheHitRate = totalInputAll > 0 ? ((data.totalCacheRead / totalInputAll) * 100).toFixed(1) : 0
242
+ const outputRatio = totalInputAll > 0 ? (data.totalOutputTokens / totalInputAll).toFixed(3) : 0
243
+ const aiVsHuman = data.totalUserChars > 0 ? (data.totalAssistantChars / data.totalUserChars).toFixed(1) : 0
244
+ return { totalTok, msgsPerSession, toolsPerSession, tokPerMsg, cacheHitRate, outputRatio, aiVsHuman }
245
+ }, [data])
246
+
247
+ const toolCategories = useMemo(() => tools.length > 0 ? categorizeTools(tools) : [], [tools])
248
+
249
+ function handleToolClick(evt, elements) {
250
+ if (elements.length > 0) {
251
+ const idx = elements[0].index
252
+ const toolName = tools[idx]?.name
253
+ if (toolName) setSelectedTool(toolName)
254
+ }
255
+ }
256
+
257
+ return (
258
+ <div className="fade-in space-y-3">
259
+ {/* Filters */}
260
+ <PageHeader icon={TrendingUp} title="Deep Analysis">
261
+ <select
262
+ value={editor}
263
+ onChange={e => setEditor(e.target.value)}
264
+ className="px-2 py-1 text-[12px] outline-none"
265
+ style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
266
+ >
267
+ <option value="">All Editors</option>
268
+ {editors.map(e => (
269
+ <option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
270
+ ))}
271
+ </select>
272
+ <select
273
+ value={folder}
274
+ onChange={e => setFolder(e.target.value)}
275
+ className="px-2 py-1 text-[12px] outline-none max-w-[200px] truncate"
276
+ style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
277
+ >
278
+ <option value="">All Projects</option>
279
+ {projects.map(p => (
280
+ <option key={p.fullPath || p.name} value={p.fullPath}>{p.name}</option>
281
+ ))}
282
+ </select>
283
+ {loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
284
+ {data && <span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions analyzed</span>}
285
+ <div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
286
+ </PageHeader>
287
+
288
+ {data && insights && (
289
+ <>
290
+ {/* KPIs */}
291
+ <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
292
+ <KpiCard label="sessions" value={data.analyzedChats} />
293
+ <KpiCard label="messages" value={formatNumber(data.totalMessages)} sub={`${insights.msgsPerSession}/session`} />
294
+ <KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} sub={`${insights.toolsPerSession}/session`} />
295
+ <KpiCard label="total tokens" value={formatNumber(insights.totalTok)} sub={`${formatNumber(insights.tokPerMsg)}/msg`} />
296
+ <KpiCard label="you wrote" value={formatNumber(data.totalUserChars)} sub={`AI: ${insights.aiVsHuman}\u00d7 more`} />
297
+ <KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '\u2014'} />
298
+ </div>
299
+
300
+ {/* Token flow + Insights row */}
301
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
302
+ {/* Token flow visualization */}
303
+ <div className="card p-3">
304
+ <SectionTitle>token flow</SectionTitle>
305
+ <div className="space-y-3 mt-2">
306
+ {/* Input tokens breakdown */}
307
+ <div>
308
+ <div className="flex items-center justify-between text-[11px] mb-1">
309
+ <span style={{ color: 'var(--c-text2)' }}>input tokens</span>
310
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite)}</span>
311
+ </div>
312
+ <ProportionBar segments={[
313
+ { label: 'Fresh input', value: data.totalInputTokens, color: '#6366f1' },
314
+ { label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
315
+ { label: 'Cache read', value: data.totalCacheRead, color: '#34d399' },
316
+ ]} />
317
+ <div className="flex items-center gap-3 mt-1 text-[10px]">
318
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens)}</span>
319
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write {formatNumber(data.totalCacheWrite)}</span>
320
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read {formatNumber(data.totalCacheRead)}</span>
321
+ </div>
322
+ </div>
323
+ {/* Output tokens */}
324
+ <div>
325
+ <div className="flex items-center justify-between text-[11px] mb-1">
326
+ <span style={{ color: 'var(--c-text2)' }}>output tokens</span>
327
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalOutputTokens)}</span>
328
+ </div>
329
+ <ProportionBar segments={[
330
+ { label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
331
+ ]} />
332
+ </div>
333
+ {/* Cache write */}
334
+ {data.totalCacheWrite > 0 && (
335
+ <div>
336
+ <div className="flex items-center justify-between text-[11px] mb-1">
337
+ <span style={{ color: 'var(--c-text2)' }}>cache write</span>
338
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalCacheWrite)}</span>
339
+ </div>
340
+ <ProportionBar segments={[
341
+ { label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
342
+ ]} />
343
+ </div>
344
+ )}
345
+ {/* Overall ratio bar */}
346
+ <div className="pt-2" style={{ borderTop: '1px solid var(--c-border)' }}>
347
+ <div className="text-[10px] mb-1" style={{ color: 'var(--c-text3)' }}>overall token distribution</div>
348
+ <ProportionBar height={10} segments={[
349
+ { label: 'Input', value: data.totalInputTokens, color: '#6366f1' },
350
+ { label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
351
+ { label: 'Cache Read', value: data.totalCacheRead, color: '#34d399' },
352
+ { label: 'Cache Write', value: data.totalCacheWrite, color: '#fbbf24' },
353
+ ]} />
354
+ <div className="flex items-center gap-3 mt-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
355
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> in</span>
356
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> out</span>
357
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read</span>
358
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write</span>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ {/* Efficiency insights */}
365
+ <div className="card p-3">
366
+ <SectionTitle>efficiency insights</SectionTitle>
367
+ <div className="space-y-3 mt-2">
368
+ {/* Human vs AI chars */}
369
+ <div>
370
+ <div className="text-[11px] mb-1" style={{ color: 'var(--c-text2)' }}>you vs AI (characters)</div>
371
+ <ProportionBar height={8} segments={[
372
+ { label: 'You', value: data.totalUserChars, color: '#6366f1' },
373
+ { label: 'AI', value: data.totalAssistantChars, color: '#34d399' },
374
+ ]} />
375
+ <div className="flex items-center justify-between mt-1 text-[10px]">
376
+ <span style={{ color: '#6366f1' }}>You: {formatNumber(data.totalUserChars)}</span>
377
+ <span style={{ color: '#34d399' }}>AI: {formatNumber(data.totalAssistantChars)}</span>
378
+ </div>
379
+ </div>
380
+
381
+ {/* Metric cards */}
382
+ <div className="grid grid-cols-2 gap-2">
383
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
384
+ <div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
385
+ <TrendingUp size={9} /> output/input ratio
386
+ </div>
387
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.outputRatio}×</div>
388
+ </div>
389
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
390
+ <div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
391
+ <Zap size={9} /> cache hit rate
392
+ </div>
393
+ <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>
394
+ </div>
395
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
396
+ <div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
397
+ <MessageSquare size={9} /> tokens per message
398
+ </div>
399
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{formatNumber(insights.tokPerMsg)}</div>
400
+ </div>
401
+ <div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
402
+ <div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
403
+ <Wrench size={9} /> tools per session
404
+ </div>
405
+ <div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.toolsPerSession}</div>
406
+ </div>
407
+ </div>
408
+
409
+ {/* AI amplification */}
410
+ <div className="p-2 rounded-sm text-center" style={{ background: 'var(--c-code-bg)' }}>
411
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>AI amplification factor</div>
412
+ <div className="text-lg font-bold" style={{ color: 'var(--c-accent)' }}>{insights.aiVsHuman}×</div>
413
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>AI writes {insights.aiVsHuman}× more than you type</div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+
419
+ {/* Charts row: Tools + Models + Tool Categories */}
420
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
421
+ {/* Tools bar */}
422
+ <div className="card p-3 lg:col-span-2">
423
+ <SectionTitle>most used tools <span style={{ color: 'var(--c-text3)' }}>(click to drill down)</span></SectionTitle>
424
+ <div style={{ height: Math.max(tools.length * 20 + 10, 120) }}>
425
+ {tools.length > 0 ? (
426
+ <Bar
427
+ ref={chartRef}
428
+ data={{
429
+ labels: tools.map(t => t.name),
430
+ datasets: [{ data: tools.map(t => t.count), backgroundColor: TOOL_COLORS.concat(MODEL_COLORS).slice(0, tools.length), borderRadius: 2 }],
431
+ }}
432
+ options={{
433
+ indexAxis: 'y',
434
+ responsive: true,
435
+ maintainAspectRatio: false,
436
+ onClick: handleToolClick,
437
+ scales: {
438
+ x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
439
+ y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
440
+ },
441
+ plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
442
+ }}
443
+ />
444
+ ) : <div className="text-[11px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>}
445
+ </div>
446
+ </div>
447
+
448
+ {/* Models doughnut */}
449
+ <div className="card p-3">
450
+ <SectionTitle>models</SectionTitle>
451
+ <div style={{ height: 160 }}>
452
+ {models.length > 0 ? (
453
+ <Doughnut
454
+ data={{
455
+ labels: models.map(m => m.name),
456
+ datasets: [{ data: models.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
457
+ }}
458
+ options={{
459
+ responsive: true, maintainAspectRatio: false, cutout: '60%',
460
+ plugins: {
461
+ legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
462
+ tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
463
+ },
464
+ }}
465
+ />
466
+ ) : <div className="text-[11px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>}
467
+ </div>
468
+
469
+ {/* Tool categories below models */}
470
+ {toolCategories.length > 0 && (
471
+ <div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--c-border)' }}>
472
+ <div className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text3)' }}>tool categories</div>
473
+ <div className="space-y-1.5">
474
+ {toolCategories.map(([cat, { tools: catTools, total }]) => (
475
+ <div key={cat} className="flex items-center gap-2 text-[11px]">
476
+ <span className="truncate flex-1" style={{ color: 'var(--c-text2)' }}>{cat}</span>
477
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{total}</span>
478
+ <span style={{ color: 'var(--c-text3)' }}>({catTools.length})</span>
479
+ </div>
480
+ ))}
481
+ </div>
482
+ </div>
483
+ )}
484
+ </div>
485
+ </div>
486
+
487
+ {/* Tool drill-down */}
488
+ {selectedTool && (
489
+ <ToolDrillDown toolName={selectedTool} folder={folder} editor={editor} dateRange={dateRange} onClose={() => setSelectedTool(null)} />
490
+ )}
491
+ </>
492
+ )}
493
+ </div>
494
+ )
495
+ }