agentlytics 0.0.7 → 0.0.10

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.
@@ -3,9 +3,10 @@ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearSca
3
3
  import { Doughnut, Bar } from 'react-chartjs-2'
4
4
  import { Loader2, X } from 'lucide-react'
5
5
  import { fetchDeepAnalytics, fetchToolCalls } from '../lib/api'
6
- import { editorLabel, editorColor, formatNumber, formatDateTime } from '../lib/constants'
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 DateRangePicker from '../components/DateRangePicker'
9
10
 
10
11
  ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
11
12
 
@@ -138,6 +139,7 @@ function ToolDrillDown({ toolName, folder, onClose }) {
138
139
  export default function DeepAnalysis({ overview }) {
139
140
  const [editor, setEditor] = useState('')
140
141
  const [folder, setFolder] = useState('')
142
+ const [dateRange, setDateRange] = useState(null)
141
143
  const [data, setData] = useState(null)
142
144
  const [loading, setLoading] = useState(false)
143
145
  const [selectedTool, setSelectedTool] = useState(null)
@@ -153,12 +155,12 @@ export default function DeepAnalysis({ overview }) {
153
155
 
154
156
  async function analyze() {
155
157
  setLoading(true)
156
- const result = await fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500 })
158
+ const result = await fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500, ...dateRangeToApiParams(dateRange) })
157
159
  setData(result)
158
160
  setLoading(false)
159
161
  }
160
162
 
161
- useEffect(() => { analyze() }, [editor, folder])
163
+ useEffect(() => { analyze() }, [editor, folder, dateRange])
162
164
 
163
165
  const tools = data?.topTools?.slice(0, 15) || []
164
166
  const models = data?.topModels?.slice(0, 10) || []
@@ -200,6 +202,7 @@ export default function DeepAnalysis({ overview }) {
200
202
  <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />
201
203
  )}
202
204
  {data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions</span>}
205
+ <div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
203
206
  </div>
204
207
 
205
208
  {data && (
@@ -0,0 +1,236 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { useSearchParams, useNavigate } from 'react-router-dom'
3
+ import { ArrowLeft, Search } 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 { fetchProjects, fetchChats } from '../lib/api'
7
+ import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
8
+ import { useTheme } from '../lib/theme'
9
+ import KpiCard from '../components/KpiCard'
10
+ import EditorDot from '../components/EditorDot'
11
+ import ChatSidebar from '../components/ChatSidebar'
12
+
13
+ ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
14
+
15
+ const MONO = 'JetBrains Mono, monospace'
16
+ const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
17
+
18
+ export default function ProjectDetail() {
19
+ const [searchParams] = useSearchParams()
20
+ const navigate = useNavigate()
21
+ const folder = searchParams.get('folder')
22
+ const { dark } = useTheme()
23
+ const txtColor = dark ? '#888' : '#555'
24
+ const txtDim = dark ? '#555' : '#999'
25
+ const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
26
+
27
+ const [project, setProject] = useState(null)
28
+ const [chats, setChats] = useState([])
29
+ const [loading, setLoading] = useState(true)
30
+ const [chatSearch, setChatSearch] = useState('')
31
+ const [selectedChatId, setSelectedChatId] = useState(null)
32
+
33
+ useEffect(() => {
34
+ if (!folder) return
35
+ setLoading(true)
36
+ Promise.all([
37
+ fetchProjects(),
38
+ fetchChats({ folder, limit: 1000 }),
39
+ ]).then(([projects, chatData]) => {
40
+ const match = projects.find(p => p.folder === folder)
41
+ setProject(match || null)
42
+ setChats(chatData.chats || [])
43
+ setLoading(false)
44
+ })
45
+ }, [folder])
46
+
47
+ const filteredChats = useMemo(() => {
48
+ if (!chatSearch) return chats
49
+ const q = chatSearch.toLowerCase()
50
+ return chats.filter(c =>
51
+ (c.name && c.name.toLowerCase().includes(q)) ||
52
+ (c.topModel && c.topModel.toLowerCase().includes(q)) ||
53
+ (c.source && c.source.toLowerCase().includes(q))
54
+ )
55
+ }, [chats, chatSearch])
56
+
57
+ if (!folder) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>no project specified</div>
58
+ if (loading) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading project...</div>
59
+ if (!project) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>project not found</div>
60
+
61
+ const editorEntries = Object.entries(project.editors).sort((a, b) => b[1] - a[1])
62
+ const maxEditorCount = editorEntries.length > 0 ? editorEntries[0][1] : 1
63
+
64
+ return (
65
+ <div className="fade-in space-y-5">
66
+ {/* Back + title */}
67
+ <div className="flex items-center gap-3">
68
+ <button
69
+ onClick={() => navigate('/projects')}
70
+ className="flex items-center gap-1.5 text-xs transition"
71
+ style={{ color: 'var(--c-text2)' }}
72
+ >
73
+ <ArrowLeft size={14} /> Projects
74
+ </button>
75
+ <div className="flex-1 min-w-0">
76
+ <h1 className="text-base font-semibold truncate" style={{ color: 'var(--c-white)' }}>{project.name}</h1>
77
+ <div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{project.folder}</div>
78
+ </div>
79
+ </div>
80
+
81
+ {/* KPIs */}
82
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
83
+ <KpiCard label="sessions" value={project.totalSessions} />
84
+ <KpiCard label="messages" value={formatNumber(project.totalMessages)} />
85
+ <KpiCard label="tool calls" value={formatNumber(project.totalToolCalls)} />
86
+ <KpiCard label="input tokens" value={formatNumber(project.totalInputTokens)} />
87
+ <KpiCard label="output tokens" value={formatNumber(project.totalOutputTokens)} />
88
+ <KpiCard label="active since" value={formatDate(project.firstSeen)} />
89
+ </div>
90
+
91
+ {/* Charts row */}
92
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
93
+ {/* Editors */}
94
+ <div className="card p-4">
95
+ <h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>editors</h3>
96
+ <div className="space-y-2">
97
+ {editorEntries.map(([e, c]) => (
98
+ <div key={e} className="flex items-center gap-2">
99
+ <span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: editorColor(e) }} />
100
+ <span className="text-xs flex-1 truncate" style={{ color: 'var(--c-text2)' }}>{editorLabel(e)}</span>
101
+ <div className="w-20 h-3 overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
102
+ <div className="h-full" style={{ width: `${(c / maxEditorCount * 100).toFixed(0)}%`, background: editorColor(e) + '60' }} />
103
+ </div>
104
+ <span className="text-[10px] w-6 text-right" style={{ color: 'var(--c-text3)' }}>{c}</span>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+
110
+ {/* Models chart */}
111
+ <div className="card p-4">
112
+ <h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>models</h3>
113
+ {project.topModels.length > 0 ? (
114
+ <div style={{ height: 180 }}>
115
+ <Doughnut
116
+ data={{
117
+ labels: project.topModels.map(m => m.name),
118
+ datasets: [{ data: project.topModels.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
119
+ }}
120
+ options={{
121
+ responsive: true, maintainAspectRatio: false, cutout: '55%',
122
+ plugins: {
123
+ legend: { position: 'right', labels: { color: txtColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
124
+ tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
125
+ },
126
+ }}
127
+ />
128
+ </div>
129
+ ) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no model data</div>}
130
+ </div>
131
+
132
+ {/* Top tools */}
133
+ <div className="card p-4">
134
+ <h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>top tools</h3>
135
+ {project.topTools.length > 0 ? (
136
+ <div style={{ height: 180 }}>
137
+ <Bar
138
+ data={{
139
+ labels: project.topTools.map(t => t.name),
140
+ datasets: [{
141
+ data: project.topTools.map(t => t.count),
142
+ backgroundColor: 'rgba(99,102,241,0.4)',
143
+ borderRadius: 2,
144
+ }],
145
+ }}
146
+ options={{
147
+ responsive: true, maintainAspectRatio: false, indexAxis: 'y',
148
+ scales: {
149
+ x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
150
+ y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
151
+ },
152
+ plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
153
+ }}
154
+ />
155
+ </div>
156
+ ) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no tool data</div>}
157
+ </div>
158
+ </div>
159
+
160
+ {/* Token breakdown */}
161
+ {(project.totalCacheRead > 0 || project.totalCacheWrite > 0) && (
162
+ <div className="flex gap-4 text-[10px]" style={{ color: 'var(--c-text3)' }}>
163
+ <span>cache read: {formatNumber(project.totalCacheRead)}</span>
164
+ <span>cache write: {formatNumber(project.totalCacheWrite)}</span>
165
+ </div>
166
+ )}
167
+
168
+ {/* Sessions list */}
169
+ <div>
170
+ <div className="flex items-center gap-3 mb-3">
171
+ <h3 className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>sessions ({chats.length})</h3>
172
+ <div className="relative max-w-xs flex-1">
173
+ <Search size={11} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
174
+ <input
175
+ type="text"
176
+ placeholder="filter sessions..."
177
+ value={chatSearch}
178
+ onChange={e => setChatSearch(e.target.value)}
179
+ className="w-full pl-7 pr-3 py-1 text-[11px] outline-none"
180
+ style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
181
+ />
182
+ </div>
183
+ </div>
184
+
185
+ <div className="card overflow-hidden">
186
+ <table className="w-full text-sm">
187
+ <thead>
188
+ <tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
189
+ <th className="text-left py-2.5 px-4 font-medium">editor</th>
190
+ <th className="text-left py-2.5 px-4 font-medium">name</th>
191
+ <th className="text-left py-2.5 px-4 font-medium">mode</th>
192
+ <th className="text-left py-2.5 px-4 font-medium">model</th>
193
+ <th className="text-left py-2.5 px-4 font-medium">updated</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ {filteredChats.map(c => (
198
+ <tr
199
+ key={c.id}
200
+ className="cursor-pointer transition"
201
+ style={{ borderBottom: '1px solid var(--c-border)' }}
202
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
203
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
204
+ onClick={() => setSelectedChatId(c.id)}
205
+ >
206
+ <td className="py-2.5 px-4">
207
+ <EditorDot source={c.source} showLabel size={7} />
208
+ </td>
209
+ <td className="py-2.5 px-4 font-medium truncate max-w-[300px]" style={{ color: 'var(--c-white)' }}>
210
+ {c.name || <span style={{ color: 'var(--c-text3)' }}>(untitled)</span>}
211
+ {c.encrypted && <span className="ml-2 text-[10px] text-yellow-500/60">locked</span>}
212
+ </td>
213
+ <td className="py-2.5 px-4">
214
+ <span className="text-xs" style={{ color: 'var(--c-text2)' }}>{c.mode}</span>
215
+ </td>
216
+ <td className="py-2.5 px-4 text-xs truncate max-w-[180px] font-mono" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>
217
+ {c.topModel || ''}
218
+ </td>
219
+ <td className="py-2.5 px-4 text-xs whitespace-nowrap" style={{ color: 'var(--c-text2)' }}>
220
+ {formatDate(c.lastUpdatedAt || c.createdAt)}
221
+ </td>
222
+ </tr>
223
+ ))}
224
+ </tbody>
225
+ </table>
226
+ {filteredChats.length === 0 && (
227
+ <div className="text-center py-8 text-sm" style={{ color: 'var(--c-text3)' }}>no sessions found</div>
228
+ )}
229
+ </div>
230
+ </div>
231
+
232
+ {/* Chat sidebar */}
233
+ <ChatSidebar chatId={selectedChatId} onClose={() => setSelectedChatId(null)} />
234
+ </div>
235
+ )
236
+ }
@@ -1,11 +1,13 @@
1
1
  import { useState, useEffect } 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 { Search, ChevronDown, ChevronUp } from 'lucide-react'
4
+ import { Search, ChevronRight } from 'lucide-react'
5
+ import { useNavigate } from 'react-router-dom'
5
6
  import { fetchProjects } from '../lib/api'
6
- import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
7
+ import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
7
8
  import { useTheme } from '../lib/theme'
8
9
  import KpiCard from '../components/KpiCard'
10
+ import DateRangePicker from '../components/DateRangePicker'
9
11
 
10
12
  ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
11
13
 
@@ -20,12 +22,13 @@ export default function Projects({ overview }) {
20
22
  const [projects, setProjects] = useState(null)
21
23
  const [search, setSearch] = useState('')
22
24
  const [editorFilter, setEditorFilter] = useState('')
23
- const [expanded, setExpanded] = useState(null)
25
+ const [dateRange, setDateRange] = useState(null)
26
+ const navigate = useNavigate()
24
27
  const editors = overview?.editors || []
25
28
 
26
29
  useEffect(() => {
27
- fetchProjects().then(setProjects)
28
- }, [])
30
+ fetchProjects(dateRangeToApiParams(dateRange)).then(setProjects)
31
+ }, [dateRange])
29
32
 
30
33
  if (!projects) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading projects...</div>
31
34
 
@@ -109,133 +112,59 @@ export default function Projects({ overview }) {
109
112
 
110
113
  {/* Filters */}
111
114
  <div className="flex items-center gap-2">
112
- <select
113
- value={editorFilter}
114
- onChange={e => setEditorFilter(e.target.value)}
115
- className="px-2 py-2 text-sm outline-none"
116
- style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
117
- >
118
- <option value="">All Editors</option>
119
- {editors.map(e => (
120
- <option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
121
- ))}
122
- </select>
123
- <div className="relative max-w-sm flex-1">
124
- <Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
125
- <input
126
- type="text"
127
- placeholder="search projects..."
128
- value={search}
129
- onChange={e => setSearch(e.target.value)}
130
- className="w-full pl-8 pr-3 py-2 text-sm outline-none"
115
+ <select
116
+ value={editorFilter}
117
+ onChange={e => setEditorFilter(e.target.value)}
118
+ className="px-2 py-1 text-[11px] outline-none"
131
119
  style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
132
- />
133
- </div>
120
+ >
121
+ <option value="">All Editors</option>
122
+ {editors.map(e => (
123
+ <option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
124
+ ))}
125
+ </select>
126
+ <div className="relative max-w-sm flex-1">
127
+ <Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
128
+ <input
129
+ type="text"
130
+ placeholder="search projects..."
131
+ value={search}
132
+ onChange={e => setSearch(e.target.value)}
133
+ className="w-full pl-8 pr-3 py-1 text-[11px] outline-none"
134
+ style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
135
+ />
136
+ </div>
137
+ <div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
134
138
  </div>
135
139
 
136
140
  {/* Project list */}
137
141
  <div className="space-y-2">
138
142
  {filtered.map(p => {
139
- const isOpen = expanded === p.folder
140
143
  const editorEntries = Object.entries(p.editors).sort((a, b) => b[1] - a[1])
141
- const maxEditorCount = editorEntries.length > 0 ? editorEntries[0][1] : 1
142
144
 
143
145
  return (
144
- <div key={p.folder} className="card overflow-hidden">
145
- {/* Header row */}
146
- <div
147
- className="flex items-center gap-3 px-4 py-3 cursor-pointer transition"
148
- onClick={() => setExpanded(isOpen ? null : p.folder)}
149
- >
150
- <div className="flex-1 min-w-0">
151
- <div className="text-sm font-medium truncate" style={{ color: 'var(--c-white)' }}>{p.name}</div>
152
- <div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
153
- </div>
154
- <div className="flex items-center gap-4 text-[10px] flex-shrink-0" style={{ color: 'var(--c-text2)' }}>
155
- <div className="flex items-center gap-1.5">
156
- {editorEntries.slice(0, 4).map(([e]) => (
157
- <span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
158
- ))}
159
- </div>
160
- <span>{p.totalSessions} sessions</span>
161
- <span>{formatNumber(p.totalMessages)} msgs</span>
162
- {p.totalToolCalls > 0 && <span>{formatNumber(p.totalToolCalls)} tools</span>}
163
- {(p.totalInputTokens + p.totalOutputTokens) > 0 && <span>{formatNumber(p.totalInputTokens + p.totalOutputTokens)} tokens</span>}
164
- <span>{formatDate(p.lastSeen)}</span>
165
- {isOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
166
- </div>
146
+ <div
147
+ key={p.folder}
148
+ className="card px-4 py-3 flex items-center gap-3 cursor-pointer transition"
149
+ onClick={() => navigate(`/projects/detail?folder=${encodeURIComponent(p.folder)}`)}
150
+ >
151
+ <div className="flex-1 min-w-0">
152
+ <div className="text-sm font-medium truncate" style={{ color: 'var(--c-white)' }}>{p.name}</div>
153
+ <div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
167
154
  </div>
168
-
169
- {/* Expanded detail */}
170
- {isOpen && (
171
- <div className="px-4 pb-4 pt-1 fade-in" style={{ borderTop: '1px solid var(--c-border)' }}>
172
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 mb-4">
173
- <KpiCard label="sessions" value={p.totalSessions} />
174
- <KpiCard label="messages" value={formatNumber(p.totalMessages)} />
175
- <KpiCard label="tool calls" value={formatNumber(p.totalToolCalls)} />
176
- <KpiCard label="input tokens" value={formatNumber(p.totalInputTokens)} />
177
- <KpiCard label="output tokens" value={formatNumber(p.totalOutputTokens)} />
178
- <KpiCard label="active since" value={formatDate(p.firstSeen)} />
179
- </div>
180
-
181
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
182
- {/* Editors */}
183
- <div>
184
- <h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>editors</h4>
185
- <div className="space-y-1.5">
186
- {editorEntries.map(([e, c]) => (
187
- <div key={e} className="flex items-center gap-2">
188
- <span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: editorColor(e) }} />
189
- <span className="text-xs flex-1 truncate" style={{ color: 'var(--c-text2)' }}>{editorLabel(e)}</span>
190
- <div className="w-24 h-3 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
191
- <div className="h-full rounded-sm" style={{ width: `${(c / maxEditorCount * 100).toFixed(0)}%`, background: editorColor(e) + '60' }} />
192
- </div>
193
- <span className="text-[10px] w-6 text-right" style={{ color: 'var(--c-text3)' }}>{c}</span>
194
- </div>
195
- ))}
196
- </div>
197
- </div>
198
-
199
- {/* Models */}
200
- <div>
201
- <h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>models</h4>
202
- {p.topModels.length > 0 ? (
203
- <div className="space-y-1.5">
204
- {p.topModels.map(m => (
205
- <div key={m.name} className="flex justify-between text-xs py-0.5">
206
- <span className="truncate" style={{ color: 'var(--c-text2)' }}>{m.name}</span>
207
- <span className="ml-2" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
208
- </div>
209
- ))}
210
- </div>
211
- ) : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>no model data</div>}
212
- </div>
213
-
214
- {/* Tools */}
215
- <div>
216
- <h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>top tools</h4>
217
- {p.topTools.length > 0 ? (
218
- <div className="space-y-1.5">
219
- {p.topTools.map(t => (
220
- <div key={t.name} className="flex justify-between text-xs py-0.5">
221
- <span className="truncate" style={{ color: 'var(--c-text2)' }}>{t.name}</span>
222
- <span className="ml-2" style={{ color: 'var(--c-text3)' }}>{t.count}</span>
223
- </div>
224
- ))}
225
- </div>
226
- ) : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>no tool data</div>}
227
- </div>
228
- </div>
229
-
230
- {/* Token breakdown */}
231
- {(p.totalCacheRead > 0 || p.totalCacheWrite > 0) && (
232
- <div className="mt-3 flex gap-4 text-[10px]" style={{ color: 'var(--c-text3)' }}>
233
- <span>cache read: {formatNumber(p.totalCacheRead)}</span>
234
- <span>cache write: {formatNumber(p.totalCacheWrite)}</span>
235
- </div>
236
- )}
155
+ <div className="flex items-center gap-4 text-[10px] flex-shrink-0" style={{ color: 'var(--c-text2)' }}>
156
+ <div className="flex items-center gap-1.5">
157
+ {editorEntries.slice(0, 4).map(([e]) => (
158
+ <span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
159
+ ))}
237
160
  </div>
238
- )}
161
+ <span>{p.totalSessions} sessions</span>
162
+ <span>{formatNumber(p.totalMessages)} msgs</span>
163
+ {p.totalToolCalls > 0 && <span>{formatNumber(p.totalToolCalls)} tools</span>}
164
+ {(p.totalInputTokens + p.totalOutputTokens) > 0 && <span>{formatNumber(p.totalInputTokens + p.totalOutputTokens)} tokens</span>}
165
+ <span>{formatDate(p.lastSeen)}</span>
166
+ <ChevronRight size={14} />
167
+ </div>
239
168
  </div>
240
169
  )
241
170
  })}