agentlytics 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editors/antigravity.js +10 -1
- package/editors/base.js +185 -0
- package/editors/claude.js +11 -1
- package/editors/codex.js +6 -0
- package/editors/commandcode.js +6 -1
- package/editors/copilot.js +6 -1
- package/editors/cursor-agent.js +6 -1
- package/editors/cursor.js +9 -1
- package/editors/gemini.js +10 -1
- package/editors/goose.js +50 -1
- package/editors/index.js +52 -1
- package/editors/kiro.js +10 -1
- package/editors/opencode.js +32 -1
- package/editors/vscode.js +13 -1
- package/editors/windsurf.js +14 -1
- package/editors/zed.js +31 -1
- package/index.js +5 -0
- package/package.json +1 -1
- package/server.js +288 -0
- package/ui/src/App.jsx +3 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/lib/api.js +7 -0
- package/ui/src/pages/Artifacts.jsx +16 -29
- package/ui/src/pages/Compare.jsx +4 -3
- package/ui/src/pages/CostAnalysis.jsx +6 -9
- package/ui/src/pages/Dashboard.jsx +15 -12
- package/ui/src/pages/DeepAnalysis.jsx +6 -5
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +1 -1
- package/ui/src/pages/Projects.jsx +9 -6
- package/ui/src/pages/RelayDashboard.jsx +4 -1
- package/ui/src/pages/RelayUserDetail.jsx +1 -1
- package/ui/src/pages/Sessions.jsx +19 -19
- package/ui/src/pages/Settings.jsx +3 -5
- package/ui/src/pages/SqlViewer.jsx +15 -16
- package/ui/src/pages/Subscriptions.jsx +4 -7
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { Plug, Server, Wrench, Terminal, Globe, FolderOpen, Search, ChevronDown, ChevronRight, Hash, Layers, ArrowUpRight, Ban, BarChart3 } 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 { fetchMCPs } from '../lib/api'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber } from '../lib/constants'
|
|
8
|
+
import EditorIcon from '../components/EditorIcon'
|
|
9
|
+
import AnimatedLoader from '../components/AnimatedLoader'
|
|
10
|
+
import SectionTitle from '../components/SectionTitle'
|
|
11
|
+
import KpiCard from '../components/KpiCard'
|
|
12
|
+
import PageHeader from '../components/PageHeader'
|
|
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 CHART_COLORS = ['#6366f1', '#22c55e', '#f59e0b', '#ec4899', '#3b82f6', '#a855f7', '#14b8a6', '#f43f5e', '#84cc16', '#06b6d4', '#e879f9', '#fb923c']
|
|
19
|
+
|
|
20
|
+
function ServerCard({ server, matchedTools }) {
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
const matched = matchedTools || []
|
|
23
|
+
const totalCalls = matched.reduce((s, t) => s + t.count, 0)
|
|
24
|
+
const queriedTools = server.tools || []
|
|
25
|
+
const hasContent = queriedTools.length > 0 || matched.length > 0
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="card overflow-hidden">
|
|
29
|
+
<button
|
|
30
|
+
onClick={() => hasContent && setOpen(!open)}
|
|
31
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[var(--c-bg3)] transition"
|
|
32
|
+
>
|
|
33
|
+
<div className="flex items-center justify-center w-6 h-6" style={{ background: `${editorColor(server.editor)}15` }}>
|
|
34
|
+
<Server size={11} style={{ color: editorColor(server.editor) }} />
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex-1 min-w-0">
|
|
37
|
+
<div className="flex items-center gap-1.5">
|
|
38
|
+
<span className="text-[12px] font-semibold truncate" style={{ color: 'var(--c-white)' }}>{server.name}</span>
|
|
39
|
+
<span className="text-[10px] px-1 py-px" style={{
|
|
40
|
+
background: server.scope === 'global' ? 'rgba(99,102,241,0.12)' : 'rgba(34,197,94,0.12)',
|
|
41
|
+
color: server.scope === 'global' ? '#818cf8' : '#22c55e',
|
|
42
|
+
}}>{server.scope}</span>
|
|
43
|
+
<span className="text-[10px] px-1 py-px" style={{
|
|
44
|
+
background: server.transport === 'stdio' ? 'rgba(234,179,8,0.12)' : 'rgba(59,130,246,0.12)',
|
|
45
|
+
color: server.transport === 'stdio' ? '#eab308' : '#3b82f6',
|
|
46
|
+
}}>{server.transport}</span>
|
|
47
|
+
{server.disabled && <span className="text-[10px] px-1 py-px" style={{ background: 'rgba(239,68,68,0.12)', color: '#ef4444' }}>disabled</span>}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex items-center gap-2 mt-px">
|
|
50
|
+
<span className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
51
|
+
<EditorIcon source={server.editor} size={10} />
|
|
52
|
+
{server.editorLabel}
|
|
53
|
+
</span>
|
|
54
|
+
{server.command && (
|
|
55
|
+
<span className="text-[10px] truncate" style={{ color: 'var(--c-text3)', fontFamily: MONO }}>
|
|
56
|
+
{server.command} {(server.args || []).slice(0, 2).join(' ')}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
{server.url && (
|
|
60
|
+
<span className="flex items-center gap-1 text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>
|
|
61
|
+
<Globe size={9} />{server.url}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="text-right shrink-0">
|
|
67
|
+
{totalCalls > 0 && <div className="text-[12px] font-semibold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalCalls)}</div>}
|
|
68
|
+
<div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
69
|
+
{queriedTools.length > 0 ? `${queriedTools.length} tools` : 'offline'}
|
|
70
|
+
{matched.length > 0 ? ` · ${matched.length} matched` : ''}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
{hasContent ? (open ? <ChevronDown size={12} style={{ color: 'var(--c-text3)' }} /> : <ChevronRight size={12} style={{ color: 'var(--c-text3)' }} />) : <div className="w-3" />}
|
|
74
|
+
</button>
|
|
75
|
+
{open && hasContent && (
|
|
76
|
+
<div className="px-3 pb-2 pt-1" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
77
|
+
{queriedTools.length > 0 && (
|
|
78
|
+
<div className="mb-1.5">
|
|
79
|
+
<div className="text-[10px] font-medium mb-0.5" style={{ color: 'var(--c-text3)' }}>
|
|
80
|
+
<Wrench size={9} className="inline mr-0.5" />Available ({queriedTools.length})
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex flex-wrap gap-0.5">
|
|
83
|
+
{queriedTools.map(t => (
|
|
84
|
+
<span key={t} className="text-[10px] px-1 py-px" style={{ background: 'var(--c-bg3)', color: 'var(--c-text3)', fontFamily: MONO }}>{t}</span>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{matched.length > 0 && (
|
|
90
|
+
<div>
|
|
91
|
+
<div className="text-[10px] font-medium mb-0.5" style={{ color: 'var(--c-text3)' }}>
|
|
92
|
+
<Hash size={9} className="inline mr-0.5" />Matched Calls
|
|
93
|
+
</div>
|
|
94
|
+
<div className="space-y-0.5">
|
|
95
|
+
{matched.map(t => (
|
|
96
|
+
<div key={t.name} className="flex items-center justify-between py-0.5 px-1.5" style={{ background: 'var(--c-bg3)' }}>
|
|
97
|
+
<span className="text-[10px] truncate" style={{ fontFamily: MONO, color: 'var(--c-text)' }}>{t.name}</span>
|
|
98
|
+
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
99
|
+
<span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>{t.sessionCount}s</span>
|
|
100
|
+
<span className="text-[11px] font-semibold" style={{ color: 'var(--c-white)' }}>{formatNumber(t.count)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default function MCPs() {
|
|
114
|
+
const navigate = useNavigate()
|
|
115
|
+
const [data, setData] = useState(null)
|
|
116
|
+
const [loading, setLoading] = useState(true)
|
|
117
|
+
const [search, setSearch] = useState('')
|
|
118
|
+
const [tab, setTab] = useState('servers') // servers | tools | sessions
|
|
119
|
+
const [toolProjectFilter, setToolProjectFilter] = useState('')
|
|
120
|
+
const [toolServerFilter, setToolServerFilter] = useState('')
|
|
121
|
+
const { dark } = useTheme()
|
|
122
|
+
const txtColor = dark ? '#a0a0a0' : '#444'
|
|
123
|
+
const txtDim = dark ? '#555' : '#999'
|
|
124
|
+
const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
|
|
125
|
+
const legendColor = dark ? '#9ca3af' : '#555'
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
fetchMCPs().then(d => { setData(d); setLoading(false) }).catch(() => setLoading(false))
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
const filteredServers = useMemo(() => {
|
|
132
|
+
if (!data) return []
|
|
133
|
+
const q = search.toLowerCase()
|
|
134
|
+
return data.servers.filter(s =>
|
|
135
|
+
!q || s.name.toLowerCase().includes(q) || s.editor.toLowerCase().includes(q) || (s.command || '').toLowerCase().includes(q)
|
|
136
|
+
)
|
|
137
|
+
}, [data, search])
|
|
138
|
+
|
|
139
|
+
// Resolve tool name → matched server name
|
|
140
|
+
const toolServerMap = useMemo(() => {
|
|
141
|
+
if (!data) return {}
|
|
142
|
+
const map = {}
|
|
143
|
+
for (const [serverName, tools] of Object.entries(data.matchedTools || {})) {
|
|
144
|
+
for (const t of tools) map[t.name] = serverName
|
|
145
|
+
}
|
|
146
|
+
return map
|
|
147
|
+
}, [data])
|
|
148
|
+
|
|
149
|
+
// Unique projects and servers for filter dropdowns
|
|
150
|
+
const toolFilterOptions = useMemo(() => {
|
|
151
|
+
if (!data) return { projects: [], servers: [] }
|
|
152
|
+
const projectSet = new Set()
|
|
153
|
+
const serverSet = new Set()
|
|
154
|
+
for (const t of data.toolCalls) {
|
|
155
|
+
for (const f of (t.folders || [])) projectSet.add(f)
|
|
156
|
+
const sn = toolServerMap[t.name]
|
|
157
|
+
if (sn) serverSet.add(sn); else serverSet.add('__builtin__')
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
projects: [...projectSet].sort((a, b) => a.split('/').pop().localeCompare(b.split('/').pop())),
|
|
161
|
+
servers: [...serverSet].filter(s => s !== '__builtin__').sort(),
|
|
162
|
+
hasBuiltIn: serverSet.has('__builtin__'),
|
|
163
|
+
}
|
|
164
|
+
}, [data, toolServerMap])
|
|
165
|
+
|
|
166
|
+
const filteredTools = useMemo(() => {
|
|
167
|
+
if (!data) return []
|
|
168
|
+
const q = search.toLowerCase()
|
|
169
|
+
return data.toolCalls.filter(t => {
|
|
170
|
+
if (q && !t.name.toLowerCase().includes(q)) return false
|
|
171
|
+
if (toolProjectFilter && !(t.folders || []).includes(toolProjectFilter)) return false
|
|
172
|
+
if (toolServerFilter === '__builtin__' && toolServerMap[t.name]) return false
|
|
173
|
+
if (toolServerFilter && toolServerFilter !== '__builtin__' && toolServerMap[t.name] !== toolServerFilter) return false
|
|
174
|
+
return true
|
|
175
|
+
})
|
|
176
|
+
}, [data, search, toolProjectFilter, toolServerFilter, toolServerMap])
|
|
177
|
+
|
|
178
|
+
const filteredSessions = useMemo(() => {
|
|
179
|
+
if (!data) return []
|
|
180
|
+
const q = search.toLowerCase()
|
|
181
|
+
return data.topSessions.filter(s =>
|
|
182
|
+
!q || (s.name || '').toLowerCase().includes(q) || s.source.toLowerCase().includes(q) || (s.folder || '').toLowerCase().includes(q)
|
|
183
|
+
)
|
|
184
|
+
}, [data, search])
|
|
185
|
+
|
|
186
|
+
// Group servers by editor
|
|
187
|
+
const serversByEditor = useMemo(() => {
|
|
188
|
+
const groups = {}
|
|
189
|
+
for (const s of filteredServers) {
|
|
190
|
+
if (!groups[s.editor]) groups[s.editor] = []
|
|
191
|
+
groups[s.editor].push(s)
|
|
192
|
+
}
|
|
193
|
+
return groups
|
|
194
|
+
}, [filteredServers])
|
|
195
|
+
|
|
196
|
+
// ── Chart data computations ──
|
|
197
|
+
|
|
198
|
+
// Most used MCP servers — stacked by editor
|
|
199
|
+
const mostUsedServersChart = useMemo(() => {
|
|
200
|
+
if (!data) return null
|
|
201
|
+
// Group calls by server name + editor
|
|
202
|
+
const serverCalls = {} // { serverName: { editor: count, ... } }
|
|
203
|
+
for (const s of data.servers) {
|
|
204
|
+
const matched = data.matchedTools[s.name] || []
|
|
205
|
+
const calls = matched.reduce((sum, t) => sum + t.count, 0)
|
|
206
|
+
if (calls === 0) continue
|
|
207
|
+
if (!serverCalls[s.name]) serverCalls[s.name] = {}
|
|
208
|
+
serverCalls[s.name][s.editor] = (serverCalls[s.name][s.editor] || 0) + calls
|
|
209
|
+
}
|
|
210
|
+
// Sort by total calls descending
|
|
211
|
+
const sorted = Object.entries(serverCalls)
|
|
212
|
+
.map(([name, byEditor]) => ({ name, byEditor, total: Object.values(byEditor).reduce((s, v) => s + v, 0) }))
|
|
213
|
+
.sort((a, b) => b.total - a.total)
|
|
214
|
+
.slice(0, 10)
|
|
215
|
+
if (sorted.length === 0) return null
|
|
216
|
+
// Collect all editors that appear
|
|
217
|
+
const allEditors = [...new Set(sorted.flatMap(s => Object.keys(s.byEditor)))]
|
|
218
|
+
return {
|
|
219
|
+
labels: sorted.map(s => s.name),
|
|
220
|
+
datasets: allEditors.map(ed => ({
|
|
221
|
+
label: editorLabel(ed),
|
|
222
|
+
data: sorted.map(s => s.byEditor[ed] || 0),
|
|
223
|
+
backgroundColor: editorColor(ed) + 'CC',
|
|
224
|
+
borderRadius: 2,
|
|
225
|
+
})),
|
|
226
|
+
}
|
|
227
|
+
}, [data])
|
|
228
|
+
|
|
229
|
+
// Never used MCP servers (no matched tool calls)
|
|
230
|
+
const neverUsedServers = useMemo(() => {
|
|
231
|
+
if (!data) return []
|
|
232
|
+
return data.servers.filter(s => {
|
|
233
|
+
const matched = data.matchedTools[s.name] || []
|
|
234
|
+
return matched.length === 0
|
|
235
|
+
})
|
|
236
|
+
}, [data])
|
|
237
|
+
|
|
238
|
+
// Most used MCP tools (only those matched to a server)
|
|
239
|
+
const mostUsedMcpTools = useMemo(() => {
|
|
240
|
+
if (!data) return []
|
|
241
|
+
const allMatched = []
|
|
242
|
+
for (const [serverName, tools] of Object.entries(data.matchedTools)) {
|
|
243
|
+
for (const t of tools) allMatched.push({ ...t, server: serverName })
|
|
244
|
+
}
|
|
245
|
+
return allMatched.sort((a, b) => b.count - a.count).slice(0, 12)
|
|
246
|
+
}, [data])
|
|
247
|
+
|
|
248
|
+
// Never used MCP tools (queried from server but no matching calls)
|
|
249
|
+
const neverUsedMcpTools = useMemo(() => {
|
|
250
|
+
if (!data) return []
|
|
251
|
+
const matchedToolNames = new Set()
|
|
252
|
+
for (const tools of Object.values(data.matchedTools)) {
|
|
253
|
+
for (const t of tools) matchedToolNames.add(t.name)
|
|
254
|
+
}
|
|
255
|
+
const unused = []
|
|
256
|
+
for (const s of data.servers) {
|
|
257
|
+
const sTools = s.tools || []
|
|
258
|
+
if (sTools.length === 0) continue
|
|
259
|
+
for (const toolName of sTools) {
|
|
260
|
+
if (!matchedToolNames.has(toolName)) {
|
|
261
|
+
unused.push({ name: toolName, server: s.name, editor: s.editor })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return unused
|
|
266
|
+
}, [data])
|
|
267
|
+
|
|
268
|
+
// Servers by editor (for doughnut)
|
|
269
|
+
const serversByEditorChart = useMemo(() => {
|
|
270
|
+
if (!data) return null
|
|
271
|
+
const counts = {}
|
|
272
|
+
for (const s of data.servers) {
|
|
273
|
+
const lbl = editorLabel(s.editor)
|
|
274
|
+
counts[lbl] = (counts[lbl] || 0) + 1
|
|
275
|
+
}
|
|
276
|
+
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1])
|
|
277
|
+
if (entries.length === 0) return null
|
|
278
|
+
return {
|
|
279
|
+
labels: entries.map(e => e[0]),
|
|
280
|
+
colors: entries.map(e => {
|
|
281
|
+
const srv = data.servers.find(s => editorLabel(s.editor) === e[0])
|
|
282
|
+
return srv ? editorColor(srv.editor) : '#666'
|
|
283
|
+
}),
|
|
284
|
+
values: entries.map(e => e[1]),
|
|
285
|
+
}
|
|
286
|
+
}, [data])
|
|
287
|
+
|
|
288
|
+
// Servers by transport (for doughnut)
|
|
289
|
+
const serversByTransport = useMemo(() => {
|
|
290
|
+
if (!data) return null
|
|
291
|
+
const counts = {}
|
|
292
|
+
for (const s of data.servers) counts[s.transport] = (counts[s.transport] || 0) + 1
|
|
293
|
+
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1])
|
|
294
|
+
if (entries.length === 0) return null
|
|
295
|
+
const tColors = { stdio: '#eab308', http: '#3b82f6', sse: '#a855f7' }
|
|
296
|
+
return {
|
|
297
|
+
labels: entries.map(e => e[0]),
|
|
298
|
+
colors: entries.map(e => tColors[e[0]] || '#666'),
|
|
299
|
+
values: entries.map(e => e[1]),
|
|
300
|
+
}
|
|
301
|
+
}, [data])
|
|
302
|
+
|
|
303
|
+
if (loading) return <AnimatedLoader label="Loading MCP data..." />
|
|
304
|
+
|
|
305
|
+
if (!data) return (
|
|
306
|
+
<div className="text-center py-20 text-[13px]" style={{ color: 'var(--c-text3)' }}>
|
|
307
|
+
Failed to load MCP data
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
const { summary, matchedTools } = data
|
|
312
|
+
const tabs = [
|
|
313
|
+
{ id: 'servers', label: 'Servers', count: data.servers.length },
|
|
314
|
+
{ id: 'tools', label: 'Tools', count: data.toolCalls.length },
|
|
315
|
+
{ id: 'sessions', label: 'Sessions', count: data.topSessions.length },
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className="fade-in space-y-3">
|
|
320
|
+
<PageHeader icon={Plug} title="MCPs" />
|
|
321
|
+
|
|
322
|
+
{/* KPIs */}
|
|
323
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
324
|
+
<KpiCard label="servers" value={summary.totalServers} sub={`${summary.editorsWithServers.length} editors`} />
|
|
325
|
+
<KpiCard label="unique tools" value={formatNumber(summary.uniqueTools)} />
|
|
326
|
+
<KpiCard label="tool calls" value={formatNumber(summary.totalToolCalls)} />
|
|
327
|
+
<KpiCard label="sessions" value={formatNumber(summary.sessionsWithTools)} />
|
|
328
|
+
<KpiCard label="matched" value={Object.values(matchedTools).reduce((s, arr) => s + arr.length, 0)} sub={`${Object.keys(matchedTools).length} servers`} />
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Charts row */}
|
|
332
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
333
|
+
{/* Most Used MCP Servers — stacked horizontal bar by editor */}
|
|
334
|
+
<div className="card p-3">
|
|
335
|
+
<SectionTitle>most used servers <span style={{ color: 'var(--c-text3)' }}>(by editor)</span></SectionTitle>
|
|
336
|
+
{mostUsedServersChart ? (
|
|
337
|
+
<div style={{ height: Math.max(mostUsedServersChart.labels.length * 22 + 10, 80) }}>
|
|
338
|
+
<Bar
|
|
339
|
+
data={mostUsedServersChart}
|
|
340
|
+
options={{
|
|
341
|
+
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
|
342
|
+
scales: {
|
|
343
|
+
x: { stacked: true, grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 9, family: MONO } } },
|
|
344
|
+
y: { stacked: true, grid: { display: false }, ticks: { color: txtColor, font: { size: 9, family: MONO } } },
|
|
345
|
+
},
|
|
346
|
+
plugins: {
|
|
347
|
+
legend: { position: 'top', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
348
|
+
tooltip: { mode: 'index', bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
349
|
+
},
|
|
350
|
+
}}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
) : <div className="text-[11px] text-center py-6" style={{ color: 'var(--c-text3)' }}>No MCP server usage</div>}
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Most Used MCP Tools — horizontal bar */}
|
|
357
|
+
<div className="card p-3">
|
|
358
|
+
<SectionTitle>most used tools</SectionTitle>
|
|
359
|
+
{mostUsedMcpTools.length > 0 ? (
|
|
360
|
+
<div style={{ height: Math.max(mostUsedMcpTools.length * 22 + 10, 80) }}>
|
|
361
|
+
<Bar
|
|
362
|
+
data={{
|
|
363
|
+
labels: mostUsedMcpTools.map(t => t.name),
|
|
364
|
+
datasets: [{
|
|
365
|
+
data: mostUsedMcpTools.map(t => t.count),
|
|
366
|
+
backgroundColor: CHART_COLORS.slice(0, mostUsedMcpTools.length),
|
|
367
|
+
borderRadius: 2,
|
|
368
|
+
}],
|
|
369
|
+
}}
|
|
370
|
+
options={{
|
|
371
|
+
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
|
372
|
+
scales: {
|
|
373
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 9, family: MONO } } },
|
|
374
|
+
y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 9, family: MONO } } },
|
|
375
|
+
},
|
|
376
|
+
plugins: { legend: { display: false }, tooltip: {
|
|
377
|
+
bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 },
|
|
378
|
+
callbacks: { afterLabel: (ctx) => `Server: ${mostUsedMcpTools[ctx.dataIndex]?.server || '?'}` },
|
|
379
|
+
}},
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
) : <div className="text-[11px] text-center py-6" style={{ color: 'var(--c-text3)' }}>No MCP tool usage</div>}
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{/* Doughnuts: by editor + by transport */}
|
|
387
|
+
<div className="space-y-2">
|
|
388
|
+
<div className="card p-3">
|
|
389
|
+
<SectionTitle>servers by editor</SectionTitle>
|
|
390
|
+
{serversByEditorChart ? (
|
|
391
|
+
<div style={{ height: 110 }}>
|
|
392
|
+
<Doughnut
|
|
393
|
+
data={{ labels: serversByEditorChart.labels, datasets: [{ data: serversByEditorChart.values, backgroundColor: serversByEditorChart.colors, borderWidth: 0 }] }}
|
|
394
|
+
options={{ responsive: true, maintainAspectRatio: false, cutout: '55%', plugins: {
|
|
395
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
396
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
397
|
+
}}}
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
) : <div className="text-[11px] text-center py-4" style={{ color: 'var(--c-text3)' }}>no data</div>}
|
|
401
|
+
</div>
|
|
402
|
+
<div className="card p-3">
|
|
403
|
+
<SectionTitle>servers by transport</SectionTitle>
|
|
404
|
+
{serversByTransport ? (
|
|
405
|
+
<div style={{ height: 90 }}>
|
|
406
|
+
<Doughnut
|
|
407
|
+
data={{ labels: serversByTransport.labels, datasets: [{ data: serversByTransport.values, backgroundColor: serversByTransport.colors, borderWidth: 0 }] }}
|
|
408
|
+
options={{ responsive: true, maintainAspectRatio: false, cutout: '55%', plugins: {
|
|
409
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
410
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
411
|
+
}}}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
) : <div className="text-[11px] text-center py-4" style={{ color: 'var(--c-text3)' }}>no data</div>}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Never Used Servers + Never Used Tools */}
|
|
420
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
|
421
|
+
<div className="card p-3">
|
|
422
|
+
<SectionTitle>never used servers <span style={{ color: 'var(--c-text3)' }}>({neverUsedServers.length})</span></SectionTitle>
|
|
423
|
+
{neverUsedServers.length > 0 ? (
|
|
424
|
+
<div className="space-y-0.5 max-h-[160px] overflow-y-auto scrollbar-thin">
|
|
425
|
+
{neverUsedServers.map((s, i) => (
|
|
426
|
+
<div key={`${s.name}-${s.editor}-${i}`} className="flex items-center gap-1.5 py-0.5 px-1.5" style={{ background: 'var(--c-bg3)' }}>
|
|
427
|
+
<Ban size={9} style={{ color: '#ef4444', opacity: 0.5 }} />
|
|
428
|
+
<span className="text-[10px] font-medium truncate" style={{ color: 'var(--c-text)' }}>{s.name}</span>
|
|
429
|
+
<span className="flex items-center gap-1 ml-auto shrink-0">
|
|
430
|
+
<EditorIcon source={s.editor} size={10} />
|
|
431
|
+
<span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>{s.editorLabel}</span>
|
|
432
|
+
</span>
|
|
433
|
+
{s.disabled && <span className="text-[9px] px-0.5" style={{ background: 'rgba(239,68,68,0.12)', color: '#ef4444' }}>off</span>}
|
|
434
|
+
</div>
|
|
435
|
+
))}
|
|
436
|
+
</div>
|
|
437
|
+
) : <div className="text-[11px] text-center py-4" style={{ color: 'var(--c-text3)' }}>All servers have been used</div>}
|
|
438
|
+
</div>
|
|
439
|
+
<div className="card p-3">
|
|
440
|
+
<SectionTitle>never used tools <span style={{ color: 'var(--c-text3)' }}>({neverUsedMcpTools.length})</span></SectionTitle>
|
|
441
|
+
{neverUsedMcpTools.length > 0 ? (
|
|
442
|
+
<div className="space-y-0.5 max-h-[160px] overflow-y-auto scrollbar-thin">
|
|
443
|
+
{neverUsedMcpTools.map((t, i) => (
|
|
444
|
+
<div key={`${t.name}-${t.server}-${i}`} className="flex items-center gap-1.5 py-0.5 px-1.5" style={{ background: 'var(--c-bg3)' }}>
|
|
445
|
+
<Ban size={9} style={{ color: '#f59e0b', opacity: 0.5 }} />
|
|
446
|
+
<span className="text-[10px] truncate" style={{ fontFamily: MONO, color: 'var(--c-text)' }}>{t.name}</span>
|
|
447
|
+
<span className="ml-auto shrink-0 text-[10px] px-1 py-px" style={{ background: 'rgba(99,102,241,0.12)', color: '#818cf8' }}>{t.server}</span>
|
|
448
|
+
</div>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
) : <div className="text-[11px] text-center py-4" style={{ color: 'var(--c-text3)' }}>All queried tools have been used</div>}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* Per-Project MCP Configs */}
|
|
456
|
+
{data.projectMcps && data.projectMcps.length > 0 && (
|
|
457
|
+
<div className="card overflow-hidden">
|
|
458
|
+
<div className="px-3 py-2" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
459
|
+
<SectionTitle>MCP servers by project <span style={{ color: 'var(--c-text3)' }}>({data.projectMcps.length})</span></SectionTitle>
|
|
460
|
+
</div>
|
|
461
|
+
<table className="w-full text-[12px]">
|
|
462
|
+
<thead>
|
|
463
|
+
<tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
464
|
+
<th className="text-left px-3 py-2 font-medium">project</th>
|
|
465
|
+
<th className="text-left px-3 py-2 font-medium">config</th>
|
|
466
|
+
<th className="text-left px-3 py-2 font-medium">servers</th>
|
|
467
|
+
<th className="text-right px-3 py-2 font-medium">mcp calls</th>
|
|
468
|
+
</tr>
|
|
469
|
+
</thead>
|
|
470
|
+
<tbody>
|
|
471
|
+
{data.projectMcps.map(p =>
|
|
472
|
+
p.configs.map((c, ci) => (
|
|
473
|
+
<tr
|
|
474
|
+
key={`${p.folder}-${c.file}`}
|
|
475
|
+
className="transition"
|
|
476
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
477
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
478
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
479
|
+
>
|
|
480
|
+
{ci === 0 ? (
|
|
481
|
+
<td className="px-3 py-2" rowSpan={p.configs.length}>
|
|
482
|
+
<span
|
|
483
|
+
className="flex items-center gap-1.5 cursor-pointer hover:underline"
|
|
484
|
+
style={{ color: 'var(--c-accent)' }}
|
|
485
|
+
onClick={() => navigate(`/projects/detail?folder=${encodeURIComponent(p.folder)}`)}
|
|
486
|
+
>
|
|
487
|
+
<FolderOpen size={11} />
|
|
488
|
+
<span className="font-medium truncate max-w-[200px]">{p.name}</span>
|
|
489
|
+
</span>
|
|
490
|
+
</td>
|
|
491
|
+
) : null}
|
|
492
|
+
<td className="px-3 py-2">
|
|
493
|
+
<span className="flex items-center gap-1.5">
|
|
494
|
+
<EditorIcon source={c.editor} size={12} />
|
|
495
|
+
<span style={{ color: 'var(--c-text2)' }}>{c.file}</span>
|
|
496
|
+
</span>
|
|
497
|
+
</td>
|
|
498
|
+
<td className="px-3 py-2">
|
|
499
|
+
<div className="flex flex-wrap gap-1">
|
|
500
|
+
{c.serverNames.map(sn => (
|
|
501
|
+
<span key={sn} className="text-[11px] px-1.5 py-px" style={{
|
|
502
|
+
background: matchedTools[sn] ? 'rgba(99,102,241,0.12)' : 'var(--c-bg3)',
|
|
503
|
+
color: matchedTools[sn] ? '#818cf8' : 'var(--c-text2)',
|
|
504
|
+
}}>
|
|
505
|
+
<Plug size={8} className="inline mr-0.5" />{sn}
|
|
506
|
+
{matchedTools[sn] && <span className="ml-1 opacity-60">{matchedTools[sn].reduce((s, t) => s + t.count, 0)}</span>}
|
|
507
|
+
</span>
|
|
508
|
+
))}
|
|
509
|
+
</div>
|
|
510
|
+
</td>
|
|
511
|
+
{ci === 0 ? (
|
|
512
|
+
<td className="px-3 py-2 text-right" rowSpan={p.configs.length}>
|
|
513
|
+
{p.mcpToolCalls > 0 ? (
|
|
514
|
+
<span className="font-semibold" style={{ color: 'var(--c-white)' }}>{formatNumber(p.mcpToolCalls)}</span>
|
|
515
|
+
) : (
|
|
516
|
+
<span style={{ color: 'var(--c-text3)' }}>—</span>
|
|
517
|
+
)}
|
|
518
|
+
</td>
|
|
519
|
+
) : null}
|
|
520
|
+
</tr>
|
|
521
|
+
))
|
|
522
|
+
)}
|
|
523
|
+
</tbody>
|
|
524
|
+
</table>
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{/* Tabs + Search */}
|
|
529
|
+
<div className="flex items-center gap-3">
|
|
530
|
+
<div className="flex gap-0.5">
|
|
531
|
+
{tabs.map(t => (
|
|
532
|
+
<button
|
|
533
|
+
key={t.id}
|
|
534
|
+
onClick={() => setTab(t.id)}
|
|
535
|
+
className="px-2.5 py-1 text-[12px] transition"
|
|
536
|
+
style={{
|
|
537
|
+
background: tab === t.id ? 'var(--c-card)' : 'transparent',
|
|
538
|
+
color: tab === t.id ? 'var(--c-white)' : 'var(--c-text3)',
|
|
539
|
+
border: tab === t.id ? '1px solid var(--c-border)' : '1px solid transparent',
|
|
540
|
+
}}
|
|
541
|
+
>
|
|
542
|
+
{t.label} <span className="opacity-40 ml-0.5">{t.count}</span>
|
|
543
|
+
</button>
|
|
544
|
+
))}
|
|
545
|
+
</div>
|
|
546
|
+
<div className="flex-1" />
|
|
547
|
+
{tab === 'tools' && (
|
|
548
|
+
<>
|
|
549
|
+
<select
|
|
550
|
+
value={toolProjectFilter}
|
|
551
|
+
onChange={e => setToolProjectFilter(e.target.value)}
|
|
552
|
+
className="px-2 py-1 text-[12px] outline-none"
|
|
553
|
+
style={{ background: 'var(--c-bg3)', border: '1px solid var(--c-border)', color: 'var(--c-text)' }}
|
|
554
|
+
>
|
|
555
|
+
<option value="">All Projects</option>
|
|
556
|
+
{toolFilterOptions.projects.map(f => (
|
|
557
|
+
<option key={f} value={f}>{f.split('/').pop()}</option>
|
|
558
|
+
))}
|
|
559
|
+
</select>
|
|
560
|
+
<select
|
|
561
|
+
value={toolServerFilter}
|
|
562
|
+
onChange={e => setToolServerFilter(e.target.value)}
|
|
563
|
+
className="px-2 py-1 text-[12px] outline-none"
|
|
564
|
+
style={{ background: 'var(--c-bg3)', border: '1px solid var(--c-border)', color: 'var(--c-text)' }}
|
|
565
|
+
>
|
|
566
|
+
<option value="">All Servers</option>
|
|
567
|
+
{toolFilterOptions.hasBuiltIn && <option value="__builtin__">built-in</option>}
|
|
568
|
+
{toolFilterOptions.servers.map(s => (
|
|
569
|
+
<option key={s} value={s}>{s}</option>
|
|
570
|
+
))}
|
|
571
|
+
</select>
|
|
572
|
+
</>
|
|
573
|
+
)}
|
|
574
|
+
<div className="relative">
|
|
575
|
+
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
576
|
+
<input
|
|
577
|
+
value={search}
|
|
578
|
+
onChange={e => setSearch(e.target.value)}
|
|
579
|
+
placeholder="Search..."
|
|
580
|
+
className="pl-7 pr-3 py-1 text-[12px] outline-none w-[180px]"
|
|
581
|
+
style={{ background: 'var(--c-bg3)', border: '1px solid var(--c-border)', color: 'var(--c-text)' }}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
{/* Servers Tab */}
|
|
587
|
+
{tab === 'servers' && (
|
|
588
|
+
<div>
|
|
589
|
+
{filteredServers.length === 0 ? (
|
|
590
|
+
<div className="text-center py-12 text-[12px]" style={{ color: 'var(--c-text3)' }}>
|
|
591
|
+
<Server size={24} className="mx-auto mb-2 opacity-30" />
|
|
592
|
+
<div>No MCP servers detected</div>
|
|
593
|
+
<div className="text-[10px] mt-0.5 opacity-60">Configure MCP servers in your editors</div>
|
|
594
|
+
</div>
|
|
595
|
+
) : (
|
|
596
|
+
Object.entries(serversByEditor).map(([editor, servers]) => (
|
|
597
|
+
<div key={editor} className="mb-3">
|
|
598
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
599
|
+
<EditorIcon source={editor} size={13} />
|
|
600
|
+
<span className="text-[12px] font-semibold" style={{ color: 'var(--c-white)' }}>{editorLabel(editor)}</span>
|
|
601
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>({servers.length})</span>
|
|
602
|
+
</div>
|
|
603
|
+
<div className="space-y-1">
|
|
604
|
+
{servers.map(s => (
|
|
605
|
+
<ServerCard key={`${s.name}-${s.editor}-${s.scope}`} server={s} matchedTools={matchedTools[s.name]} />
|
|
606
|
+
))}
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
))
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
|
|
614
|
+
{/* Tools Tab */}
|
|
615
|
+
{tab === 'tools' && (
|
|
616
|
+
<div className="card overflow-hidden">
|
|
617
|
+
<table className="w-full text-[12px]">
|
|
618
|
+
<thead>
|
|
619
|
+
<tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
620
|
+
<th className="text-left px-3 py-2 font-medium">tool</th>
|
|
621
|
+
<th className="text-right px-3 py-2 font-medium">calls</th>
|
|
622
|
+
<th className="text-right px-3 py-2 font-medium">sessions</th>
|
|
623
|
+
<th className="text-left px-3 py-2 font-medium">editors</th>
|
|
624
|
+
<th className="text-left px-3 py-2 font-medium">server</th>
|
|
625
|
+
</tr>
|
|
626
|
+
</thead>
|
|
627
|
+
<tbody>
|
|
628
|
+
{filteredTools.slice(0, 100).map((t, i) => {
|
|
629
|
+
const sn = toolServerMap[t.name]
|
|
630
|
+
return (
|
|
631
|
+
<tr
|
|
632
|
+
key={t.name}
|
|
633
|
+
className="transition"
|
|
634
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
635
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
636
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
637
|
+
>
|
|
638
|
+
<td className="px-3 py-2">
|
|
639
|
+
<span style={{ fontFamily: MONO, color: 'var(--c-white)' }}>{t.name}</span>
|
|
640
|
+
</td>
|
|
641
|
+
<td className="px-3 py-2 text-right">
|
|
642
|
+
<span className="font-semibold" style={{ color: 'var(--c-white)' }}>{formatNumber(t.count)}</span>
|
|
643
|
+
</td>
|
|
644
|
+
<td className="px-3 py-2 text-right" style={{ color: 'var(--c-text2)' }}>{t.sessionCount}</td>
|
|
645
|
+
<td className="px-3 py-2">
|
|
646
|
+
<div className="flex items-center gap-1">
|
|
647
|
+
{t.editors.map(e => (
|
|
648
|
+
<span key={e} className="inline-flex items-center gap-0.5">
|
|
649
|
+
<EditorIcon source={e} size={12} />
|
|
650
|
+
</span>
|
|
651
|
+
))}
|
|
652
|
+
</div>
|
|
653
|
+
</td>
|
|
654
|
+
<td className="px-3 py-2">
|
|
655
|
+
{sn ? (
|
|
656
|
+
<span className="text-[11px] px-1.5 py-px" style={{ background: 'rgba(99,102,241,0.12)', color: '#818cf8' }}>
|
|
657
|
+
<Plug size={9} className="inline mr-0.5" />{sn}
|
|
658
|
+
</span>
|
|
659
|
+
) : (
|
|
660
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>built-in</span>
|
|
661
|
+
)}
|
|
662
|
+
</td>
|
|
663
|
+
</tr>
|
|
664
|
+
)
|
|
665
|
+
})}
|
|
666
|
+
</tbody>
|
|
667
|
+
</table>
|
|
668
|
+
{filteredTools.length > 100 && (
|
|
669
|
+
<div className="px-3 py-1 text-[10px] text-center" style={{ color: 'var(--c-text3)', background: 'var(--c-card)' }}>
|
|
670
|
+
Showing 100 of {filteredTools.length}
|
|
671
|
+
</div>
|
|
672
|
+
)}
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
|
|
676
|
+
{/* Sessions Tab */}
|
|
677
|
+
{tab === 'sessions' && (
|
|
678
|
+
<div className="card overflow-hidden">
|
|
679
|
+
<table className="w-full text-[12px]">
|
|
680
|
+
<thead>
|
|
681
|
+
<tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
682
|
+
<th className="text-left px-3 py-2 font-medium">session</th>
|
|
683
|
+
<th className="text-left px-3 py-2 font-medium">editor</th>
|
|
684
|
+
<th className="text-left px-3 py-2 font-medium">project</th>
|
|
685
|
+
<th className="text-right px-3 py-2 font-medium">calls</th>
|
|
686
|
+
<th className="text-left px-3 py-2 font-medium">top tools</th>
|
|
687
|
+
</tr>
|
|
688
|
+
</thead>
|
|
689
|
+
<tbody>
|
|
690
|
+
{filteredSessions.map((s, i) => {
|
|
691
|
+
const topTools = Object.entries(s.tools).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
692
|
+
return (
|
|
693
|
+
<tr
|
|
694
|
+
key={s.composerId}
|
|
695
|
+
className="transition"
|
|
696
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
697
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
698
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
699
|
+
>
|
|
700
|
+
<td className="px-3 py-2 max-w-[220px] font-medium">
|
|
701
|
+
<span className="truncate block" style={{ color: 'var(--c-white)' }}>
|
|
702
|
+
{s.name || <span style={{ color: 'var(--c-text3)', fontStyle: 'italic' }}>Untitled</span>}
|
|
703
|
+
</span>
|
|
704
|
+
</td>
|
|
705
|
+
<td className="px-3 py-2">
|
|
706
|
+
<span className="flex items-center gap-1">
|
|
707
|
+
<EditorIcon source={s.source} size={12} />
|
|
708
|
+
<span style={{ color: 'var(--c-text2)' }}>{editorLabel(s.source)}</span>
|
|
709
|
+
</span>
|
|
710
|
+
</td>
|
|
711
|
+
<td className="px-3 py-2 truncate max-w-[160px]" title={s.folder}>
|
|
712
|
+
{s.folder ? (
|
|
713
|
+
<span
|
|
714
|
+
className="cursor-pointer hover:underline"
|
|
715
|
+
style={{ color: 'var(--c-accent)' }}
|
|
716
|
+
onClick={e => { e.stopPropagation(); navigate(`/projects/detail?folder=${encodeURIComponent(s.folder)}`) }}
|
|
717
|
+
>{s.folder.split(/[/\\]/).pop()}</span>
|
|
718
|
+
) : <span style={{ color: 'var(--c-text3)' }}>—</span>}
|
|
719
|
+
</td>
|
|
720
|
+
<td className="px-3 py-2 text-right">
|
|
721
|
+
<span className="font-semibold" style={{ color: 'var(--c-white)' }}>{formatNumber(s.totalToolCalls)}</span>
|
|
722
|
+
</td>
|
|
723
|
+
<td className="px-3 py-2">
|
|
724
|
+
<div className="flex gap-0.5 flex-wrap">
|
|
725
|
+
{topTools.map(([name, count]) => (
|
|
726
|
+
<span key={name} className="text-[10px] px-1 py-px" style={{ background: 'var(--c-bg3)', color: 'var(--c-text2)', fontFamily: MONO }}>
|
|
727
|
+
{name} <span style={{ opacity: 0.5 }}>×{count}</span>
|
|
728
|
+
</span>
|
|
729
|
+
))}
|
|
730
|
+
</div>
|
|
731
|
+
</td>
|
|
732
|
+
</tr>
|
|
733
|
+
)
|
|
734
|
+
})}
|
|
735
|
+
</tbody>
|
|
736
|
+
</table>
|
|
737
|
+
{data.topSessions.length === 0 && (
|
|
738
|
+
<div className="text-center py-8 text-[11px]" style={{ color: 'var(--c-text3)' }}>No sessions with tool calls</div>
|
|
739
|
+
)}
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
</div>
|
|
743
|
+
)
|
|
744
|
+
}
|