memhouse 0.0.1 → 0.3.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 (96) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -3
  3. package/bin/memhouse.js +487 -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/antigravity.js +1019 -0
  9. package/editors/base.js +321 -0
  10. package/editors/claude.js +386 -0
  11. package/editors/codebuff.js +338 -0
  12. package/editors/codex.js +537 -0
  13. package/editors/commandcode.js +166 -0
  14. package/editors/copilot-jetbrains.js +207 -0
  15. package/editors/copilot.js +260 -0
  16. package/editors/cursor-agent.js +202 -0
  17. package/editors/cursor.js +438 -0
  18. package/editors/gemini.js +231 -0
  19. package/editors/goose.js +358 -0
  20. package/editors/gsd.js +366 -0
  21. package/editors/index.js +179 -0
  22. package/editors/kiro.js +315 -0
  23. package/editors/opencode.js +374 -0
  24. package/editors/vscode.js +413 -0
  25. package/editors/windsurf.js +634 -0
  26. package/editors/zed.js +213 -0
  27. package/index.js +424 -2
  28. package/mcp-server.js +294 -0
  29. package/mem-house/DESIGN.md +119 -0
  30. package/mem-house/delivery/AGENT-INSTALL.md +75 -0
  31. package/mem-house/delivery/PROMPT.md +42 -0
  32. package/mem-house/delivery/install.sh +181 -0
  33. package/mem-house/delivery/kernel-install.md +76 -0
  34. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  35. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  36. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  37. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  38. package/mem-house/rls.sql +17 -0
  39. package/mem-house/schema.sql +145 -0
  40. package/mem-house/server/queries.js +878 -0
  41. package/mem-house/server/server.js +228 -0
  42. package/mem-house/shipper/ship.js +402 -0
  43. package/misc/syntax-check.js +38 -0
  44. package/package.json +56 -7
  45. package/pricing.js +88 -0
  46. package/pricing.json +992 -0
  47. package/public/assets/index-BsgjOSTv.js +75 -0
  48. package/public/assets/index-DV6ONi_F.css +2 -0
  49. package/public/index.html +16 -0
  50. package/relay-client.js +306 -0
  51. package/relay-server.js +577 -0
  52. package/server.js +860 -0
  53. package/share-image.js +367 -0
  54. package/ui/eslint.config.js +29 -0
  55. package/ui/index.html +15 -0
  56. package/ui/package-lock.json +4229 -0
  57. package/ui/package.json +38 -0
  58. package/ui/src/App.jsx +364 -0
  59. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  60. package/ui/src/components/AiAuditCard.jsx +254 -0
  61. package/ui/src/components/AnimatedLoader.jsx +14 -0
  62. package/ui/src/components/AnimatedLogo.jsx +96 -0
  63. package/ui/src/components/ChatSidebar.jsx +226 -0
  64. package/ui/src/components/DateRangePicker.jsx +104 -0
  65. package/ui/src/components/EditorDot.jsx +11 -0
  66. package/ui/src/components/EditorIcon.jsx +117 -0
  67. package/ui/src/components/KpiCard.jsx +9 -0
  68. package/ui/src/components/LiveFeed.jsx +128 -0
  69. package/ui/src/components/LoginScreen.jsx +81 -0
  70. package/ui/src/components/MessageRenderer.jsx +148 -0
  71. package/ui/src/components/PageHeader.jsx +11 -0
  72. package/ui/src/components/SectionTitle.jsx +3 -0
  73. package/ui/src/components/ShareModal.jsx +305 -0
  74. package/ui/src/components/TokenTimeline.jsx +258 -0
  75. package/ui/src/hooks/useLive.jsx +15 -0
  76. package/ui/src/index.css +83 -0
  77. package/ui/src/lib/api.js +343 -0
  78. package/ui/src/lib/constants.js +88 -0
  79. package/ui/src/lib/theme.jsx +23 -0
  80. package/ui/src/main.jsx +16 -0
  81. package/ui/src/pages/Artifacts.jsx +575 -0
  82. package/ui/src/pages/Compare.jsx +232 -0
  83. package/ui/src/pages/CostAnalysis.jsx +354 -0
  84. package/ui/src/pages/Dashboard.jsx +529 -0
  85. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  86. package/ui/src/pages/GSD.jsx +726 -0
  87. package/ui/src/pages/MCPs.jsx +744 -0
  88. package/ui/src/pages/ProjectDetail.jsx +325 -0
  89. package/ui/src/pages/Projects.jsx +249 -0
  90. package/ui/src/pages/RelayDashboard.jsx +590 -0
  91. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  92. package/ui/src/pages/Sessions.jsx +535 -0
  93. package/ui/src/pages/Settings.jsx +241 -0
  94. package/ui/src/pages/SqlViewer.jsx +344 -0
  95. package/ui/src/pages/Subscriptions.jsx +411 -0
  96. package/ui/vite.config.js +18 -0
@@ -0,0 +1,466 @@
1
+ import { useState, useEffect, useCallback, useMemo } from 'react'
2
+ import { useParams, useNavigate } from 'react-router-dom'
3
+ import { ArrowLeft, MessageSquare, FolderOpen, ChevronDown, ChevronRight, Hash } from 'lucide-react'
4
+ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
5
+ import { Doughnut, Bar } from 'react-chartjs-2'
6
+ import KpiCard from '../components/KpiCard'
7
+ import EditorIcon from '../components/EditorIcon'
8
+ import SectionTitle from '../components/SectionTitle'
9
+ import ChatSidebar from '../components/ChatSidebar'
10
+ import LiveFeed from '../components/LiveFeed'
11
+ import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
12
+ import { fetchRelayUserActivity, fetchRelaySession } from '../lib/api'
13
+ import { useTheme } from '../lib/theme'
14
+
15
+ ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
16
+
17
+ const MONO = 'JetBrains Mono, monospace'
18
+ const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
19
+
20
+ function ProportionBar({ segments, height = 6 }) {
21
+ const total = segments.reduce((s, seg) => s + seg.value, 0)
22
+ if (total === 0) return null
23
+ return (
24
+ <div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
25
+ {segments.filter(s => s.value > 0).map((seg, i) => (
26
+ <div
27
+ key={i}
28
+ title={`${seg.label}: ${formatNumber(seg.value)}`}
29
+ className="h-full transition-all"
30
+ style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
31
+ />
32
+ ))}
33
+ </div>
34
+ )
35
+ }
36
+
37
+ // Left sidebar: sessions grouped by project (folder-tree)
38
+ function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
39
+ const [collapsed, setCollapsed] = useState(new Set())
40
+
41
+ const toggle = (key) => {
42
+ setCollapsed(prev => {
43
+ const next = new Set(prev)
44
+ next.has(key) ? next.delete(key) : next.add(key)
45
+ return next
46
+ })
47
+ }
48
+
49
+ // Group sessions by project
50
+ const grouped = useMemo(() => {
51
+ const map = {}
52
+ const noProject = []
53
+ for (const s of sessions) {
54
+ if (s.folder) {
55
+ if (!map[s.folder]) map[s.folder] = []
56
+ map[s.folder].push(s)
57
+ } else {
58
+ noProject.push(s)
59
+ }
60
+ }
61
+ const sorted = Object.entries(map).sort((a, b) => b[1].length - a[1].length)
62
+ return { sorted, noProject }
63
+ }, [sessions])
64
+
65
+ const SessionItem = ({ s }) => (
66
+ <div
67
+ className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition rounded-sm"
68
+ style={{ background: selectedChat === s.id ? 'rgba(99,102,241,0.12)' : 'transparent' }}
69
+ onMouseEnter={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'var(--c-bg3)' }}
70
+ onMouseLeave={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'transparent' }}
71
+ onClick={() => onSelectChat(s.id)}
72
+ >
73
+ <EditorIcon source={s.source} size={10} />
74
+ <div className="flex-1 min-w-0">
75
+ <div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
76
+ <div className="flex items-center gap-1 text-[8px]" style={{ color: 'var(--c-text3)' }}>
77
+ {s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
78
+ {s.totalMessages > 0 && s.lastUpdatedAt && <span>·</span>}
79
+ {s.lastUpdatedAt && <span>{formatDate(s.lastUpdatedAt)}</span>}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ )
84
+
85
+ return (
86
+ <div className="flex flex-col h-full">
87
+ <div className="flex items-center gap-2 px-3 py-2.5 shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
88
+ <Hash size={12} style={{ color: 'var(--c-accent)' }} />
89
+ <span className="text-[12px] font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>Sessions</span>
90
+ <span className="text-[10px] ml-auto" style={{ color: 'var(--c-text3)' }}>{sessions.length}</span>
91
+ </div>
92
+
93
+ <div className="flex-1 overflow-y-auto scrollbar-thin py-1">
94
+ {grouped.sorted.map(([folder, list]) => {
95
+ const isCollapsed = collapsed.has(folder)
96
+ const folderName = folder.split('/').pop()
97
+ return (
98
+ <div key={folder}>
99
+ <div
100
+ className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition"
101
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
102
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
103
+ onClick={() => toggle(folder)}
104
+ >
105
+ {isCollapsed ? <ChevronRight size={10} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={10} style={{ color: 'var(--c-text3)' }} />}
106
+ <FolderOpen size={10} style={{ color: '#818cf8' }} />
107
+ <span className="text-[11px] font-medium truncate flex-1" style={{ color: 'var(--c-text2)' }} title={folder}>{folderName}</span>
108
+ <span className="text-[8px]" style={{ color: 'var(--c-text3)' }}>{list.length}</span>
109
+ </div>
110
+ {!isCollapsed && (
111
+ <div className="pl-3">
112
+ {list.map(s => <SessionItem key={s.id} s={s} />)}
113
+ </div>
114
+ )}
115
+ </div>
116
+ )
117
+ })}
118
+
119
+ {grouped.noProject.length > 0 && (
120
+ <div>
121
+ {grouped.sorted.length > 0 && (
122
+ <div className="px-2 py-1.5 text-[10px] uppercase tracking-wider" style={{ color: 'var(--c-text3)' }}>no project</div>
123
+ )}
124
+ <div className={grouped.sorted.length > 0 ? 'pl-1' : ''}>
125
+ {grouped.noProject.map(s => <SessionItem key={s.id} s={s} />)}
126
+ </div>
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export default function RelayUserDetail() {
135
+ const { username } = useParams()
136
+ const navigate = useNavigate()
137
+ const [sessions, setSessions] = useState(null)
138
+ const [selectedChat, setSelectedChat] = useState(null)
139
+ const { dark } = useTheme()
140
+
141
+ const legendColor = dark ? '#888' : '#555'
142
+ const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
143
+ const txtDim = dark ? '#555' : '#999'
144
+
145
+ useEffect(() => {
146
+ if (username) {
147
+ fetchRelayUserActivity(username, { limit: 200 }).then(setSessions)
148
+ }
149
+ }, [username])
150
+
151
+ const fetchFn = useCallback(
152
+ (id) => fetchRelaySession(id, username),
153
+ [username]
154
+ )
155
+
156
+ if (!sessions) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading...</div>
157
+
158
+ // Aggregate stats
159
+ const totalSessions = sessions.length
160
+ const totalMessages = sessions.reduce((s, c) => s + (c.totalMessages || 0), 0)
161
+ const totalInputTokens = sessions.reduce((s, c) => s + (c.totalInputTokens || 0), 0)
162
+ const totalOutputTokens = sessions.reduce((s, c) => s + (c.totalOutputTokens || 0), 0)
163
+ const totalTokens = totalInputTokens + totalOutputTokens
164
+ const msgsPerSession = totalSessions > 0 ? (totalMessages / totalSessions).toFixed(1) : 0
165
+ const tokPerSession = totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0
166
+
167
+ // Editor breakdown
168
+ const editorMap = {}
169
+ for (const s of sessions) {
170
+ if (s.source) editorMap[s.source] = (editorMap[s.source] || 0) + 1
171
+ }
172
+ const editors = Object.entries(editorMap).sort((a, b) => b[1] - a[1])
173
+
174
+ // Project breakdown
175
+ const projectMap = {}
176
+ for (const s of sessions) {
177
+ if (s.folder) {
178
+ if (!projectMap[s.folder]) projectMap[s.folder] = { count: 0, lastActive: 0, messages: 0, tokens: 0 }
179
+ projectMap[s.folder].count++
180
+ projectMap[s.folder].messages += s.totalMessages || 0
181
+ projectMap[s.folder].tokens += (s.totalInputTokens || 0) + (s.totalOutputTokens || 0)
182
+ if (s.lastUpdatedAt > projectMap[s.folder].lastActive) projectMap[s.folder].lastActive = s.lastUpdatedAt
183
+ }
184
+ }
185
+ const projects = Object.entries(projectMap).sort((a, b) => b[1].count - a[1].count)
186
+ const maxProjectSessions = projects.length > 0 ? Math.max(...projects.map(([, p]) => p.count)) : 1
187
+
188
+ // Model breakdown
189
+ const modelMap = {}
190
+ for (const s of sessions) {
191
+ if (s.models) {
192
+ for (const m of s.models) modelMap[m] = (modelMap[m] || 0) + 1
193
+ }
194
+ }
195
+ const models = Object.entries(modelMap).sort((a, b) => b[1] - a[1]).slice(0, 10)
196
+
197
+ // Tool breakdown
198
+ const toolMap = {}
199
+ for (const s of sessions) {
200
+ if (s.toolCalls) {
201
+ for (const t of s.toolCalls) toolMap[t] = (toolMap[t] || 0) + 1
202
+ }
203
+ }
204
+ const tools = Object.entries(toolMap).sort((a, b) => b[1] - a[1]).slice(0, 15)
205
+
206
+ // Daily activity (sessions per day)
207
+ const dayMap = {}
208
+ for (const s of sessions) {
209
+ const d = new Date(s.lastUpdatedAt || s.createdAt).toISOString().slice(0, 10)
210
+ dayMap[d] = (dayMap[d] || 0) + 1
211
+ }
212
+ const days = Object.entries(dayMap).sort((a, b) => a[0].localeCompare(b[0])).slice(-14)
213
+
214
+ const handleFeedClick = (chatId) => {
215
+ setSelectedChat(chatId)
216
+ }
217
+
218
+ const sidebarH = 'calc(100vh - 42px)'
219
+
220
+ return (
221
+ <div className="fade-in flex" style={{ height: sidebarH }}>
222
+ {/* ── Left sidebar: Sessions tree ── */}
223
+ <div
224
+ className="hidden lg:flex flex-col w-[250px] shrink-0 sticky top-[42px] self-start"
225
+ style={{ height: sidebarH, borderRight: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
226
+ >
227
+ <SessionSidebar sessions={sessions} projects={projects} selectedChat={selectedChat} onSelectChat={setSelectedChat} />
228
+ </div>
229
+
230
+ {/* ── Center: scrollable content ── */}
231
+ <div className="flex-1 min-w-0 overflow-y-auto p-4 space-y-3">
232
+ {/* Header */}
233
+ <div className="flex items-center gap-3">
234
+ <button
235
+ onClick={() => navigate('/')}
236
+ className="p-1.5 transition hover:bg-[var(--c-card)] rounded-sm"
237
+ style={{ border: '1px solid var(--c-border)', color: 'var(--c-text2)' }}
238
+ >
239
+ <ArrowLeft size={14} />
240
+ </button>
241
+ <div className="flex items-center gap-2.5">
242
+ <div className="w-9 h-9 flex items-center justify-center text-[14px] font-bold rounded-sm" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
243
+ {username.charAt(0).toUpperCase()}
244
+ </div>
245
+ <div>
246
+ <div className="text-[14px] font-bold" style={{ color: 'var(--c-white)' }}>{username}</div>
247
+ <div className="flex items-center gap-2 mt-0.5">
248
+ {editors.map(([src]) => (
249
+ <EditorIcon key={src} source={src} size={11} />
250
+ ))}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ {/* KPIs */}
257
+ <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
258
+ <KpiCard label="sessions" value={totalSessions} />
259
+ <KpiCard label="messages" value={formatNumber(totalMessages)} sub={`${msgsPerSession}/session`} />
260
+ <KpiCard label="editors" value={editors.length} />
261
+ <KpiCard label="projects" value={projects.length} />
262
+ <KpiCard label="tokens" value={formatNumber(totalTokens)} sub={`${formatNumber(tokPerSession)}/session`} />
263
+ <KpiCard label="models" value={models.length} />
264
+ </div>
265
+
266
+ {/* Token overview */}
267
+ {totalTokens > 0 && (
268
+ <div className="card p-3">
269
+ <div className="flex items-center justify-between mb-2">
270
+ <SectionTitle>token usage</SectionTitle>
271
+ <span className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalTokens)} total</span>
272
+ </div>
273
+ <ProportionBar height={10} segments={[
274
+ { label: 'Input', value: totalInputTokens, color: '#6366f1' },
275
+ { label: 'Output', value: totalOutputTokens, color: '#a78bfa' },
276
+ ]} />
277
+ <div className="flex items-center gap-4 mt-1.5 text-[10px]">
278
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> input {formatNumber(totalInputTokens)}</span>
279
+ <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> output {formatNumber(totalOutputTokens)}</span>
280
+ </div>
281
+ </div>
282
+ )}
283
+
284
+ {/* Charts: editors + models */}
285
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
286
+ {editors.length > 0 && (
287
+ <div className="card p-3">
288
+ <SectionTitle>editors</SectionTitle>
289
+ <div style={{ height: 160 }}>
290
+ <Doughnut
291
+ data={{
292
+ labels: editors.map(([src]) => editorLabel(src)),
293
+ datasets: [{ data: editors.map(([, c]) => c), backgroundColor: editors.map(([src]) => editorColor(src)), borderWidth: 0 }],
294
+ }}
295
+ options={{
296
+ responsive: true, maintainAspectRatio: false, cutout: '60%',
297
+ plugins: {
298
+ legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
299
+ tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
300
+ },
301
+ }}
302
+ />
303
+ </div>
304
+ </div>
305
+ )}
306
+ {models.length > 0 && (
307
+ <div className="card p-3">
308
+ <SectionTitle>models</SectionTitle>
309
+ <div style={{ height: 160 }}>
310
+ <Doughnut
311
+ data={{
312
+ labels: models.map(([name]) => name),
313
+ datasets: [{ data: models.map(([, c]) => c), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
314
+ }}
315
+ options={{
316
+ responsive: true, maintainAspectRatio: false, cutout: '60%',
317
+ plugins: {
318
+ legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
319
+ tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
320
+ },
321
+ }}
322
+ />
323
+ </div>
324
+ </div>
325
+ )}
326
+ </div>
327
+
328
+ {/* Daily activity bar */}
329
+ {days.length > 1 && (
330
+ <div className="card p-3">
331
+ <SectionTitle>daily activity (last 14 days)</SectionTitle>
332
+ <div style={{ height: 120 }}>
333
+ <Bar
334
+ data={{
335
+ labels: days.map(([d]) => d.slice(5)),
336
+ datasets: [{
337
+ data: days.map(([, c]) => c),
338
+ backgroundColor: '#6366f1',
339
+ borderRadius: 2,
340
+ }],
341
+ }}
342
+ options={{
343
+ responsive: true, maintainAspectRatio: false,
344
+ plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
345
+ scales: {
346
+ x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
347
+ y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO }, stepSize: 1 }, beginAtZero: true },
348
+ },
349
+ }}
350
+ />
351
+ </div>
352
+ </div>
353
+ )}
354
+
355
+ {/* Projects */}
356
+ {projects.length > 0 && (
357
+ <div>
358
+ <SectionTitle>projects</SectionTitle>
359
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2">
360
+ {projects.map(([folder, info]) => (
361
+ <div key={folder} className="card px-3 py-2.5">
362
+ <div className="flex items-center gap-1.5 mb-1.5">
363
+ <FolderOpen size={11} style={{ color: '#818cf8' }} />
364
+ <span className="text-[12px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{folder.split('/').pop()}</span>
365
+ </div>
366
+ <div className="text-[8px] truncate mb-2" style={{ color: 'var(--c-text3)' }}>{folder}</div>
367
+ <div className="grid grid-cols-3 gap-1 text-center mb-1.5">
368
+ <div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
369
+ <div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{info.count}</div>
370
+ <div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>sessions</div>
371
+ </div>
372
+ <div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
373
+ <div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.messages)}</div>
374
+ <div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>messages</div>
375
+ </div>
376
+ <div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
377
+ <div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.tokens)}</div>
378
+ <div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>tokens</div>
379
+ </div>
380
+ </div>
381
+ <div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
382
+ <div className="h-full rounded-full" style={{ width: `${(info.count / maxProjectSessions * 100).toFixed(0)}%`, background: '#6366f1' }} />
383
+ </div>
384
+ <div className="text-[8px] mt-1" style={{ color: 'var(--c-text3)' }}>{formatDate(info.lastActive)}</div>
385
+ </div>
386
+ ))}
387
+ </div>
388
+ </div>
389
+ )}
390
+
391
+ {/* Top tools */}
392
+ {tools.length > 0 && (
393
+ <div className="card p-3">
394
+ <SectionTitle>top tools</SectionTitle>
395
+ <div className="flex flex-wrap gap-1.5 mt-1">
396
+ {tools.map(([name, count]) => (
397
+ <span key={name} className="text-[10px] px-2 py-1 rounded-sm" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
398
+ {name} <span style={{ color: 'var(--c-text3)' }}>×{count}</span>
399
+ </span>
400
+ ))}
401
+ </div>
402
+ </div>
403
+ )}
404
+
405
+ {/* Recent sessions table */}
406
+ <div className="card p-3">
407
+ <SectionTitle>recent sessions</SectionTitle>
408
+ <div className="max-h-[400px] overflow-y-auto scrollbar-thin">
409
+ <table className="w-full text-[12px]">
410
+ <tbody>
411
+ {sessions.slice(0, 50).map(s => (
412
+ <tr
413
+ key={s.id}
414
+ className="cursor-pointer transition"
415
+ style={{ borderBottom: '1px solid var(--c-border)' }}
416
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
417
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
418
+ onClick={() => setSelectedChat(s.id)}
419
+ >
420
+ <td className="py-2 px-2 w-[24px]"><EditorIcon source={s.source} size={11} /></td>
421
+ <td className="py-2 px-2">
422
+ <div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
423
+ <div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{s.folder ? s.folder.split('/').pop() : ''}</div>
424
+ </td>
425
+ <td className="py-2 px-2 text-[10px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
426
+ {s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
427
+ </td>
428
+ <td className="py-2 px-2">
429
+ {s.mode && (
430
+ <span className="text-[8px] px-1.5 py-0.5 rounded-sm" style={{ background: 'rgba(168,85,247,0.1)', color: '#a855f7' }}>{s.mode}</span>
431
+ )}
432
+ </td>
433
+ <td className="py-2 px-2 text-[10px] whitespace-nowrap text-right" style={{ color: 'var(--c-text3)' }}>
434
+ {formatDate(s.lastUpdatedAt)}
435
+ </td>
436
+ </tr>
437
+ ))}
438
+ </tbody>
439
+ </table>
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ {/* ── Right sidebar: Live Feed ── */}
445
+ <div
446
+ className="hidden xl:flex flex-col w-[300px] shrink-0 sticky top-[42px] self-start"
447
+ style={{ height: sidebarH, borderLeft: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
448
+ >
449
+ <LiveFeed onSessionClick={handleFeedClick} />
450
+ </div>
451
+
452
+ {/* Session sidebar */}
453
+ <ChatSidebar
454
+ chatId={selectedChat}
455
+ onClose={() => setSelectedChat(null)}
456
+ fetchFn={fetchFn}
457
+ username={username}
458
+ extraHeader={
459
+ <span className="text-[11px] font-medium px-1.5 py-0.5 shrink-0" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
460
+ {username}
461
+ </span>
462
+ }
463
+ />
464
+ </div>
465
+ )
466
+ }