agentlytics 0.1.1 → 0.1.3

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