@geminilight/mindos 0.5.64 → 0.5.66

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 (86) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/file/route.ts +9 -0
  5. package/app/app/api/mcp/agents/route.ts +27 -1
  6. package/app/app/api/skills/route.ts +18 -2
  7. package/app/app/api/tree-version/route.ts +8 -0
  8. package/app/components/ActivityBar.tsx +2 -2
  9. package/app/components/Backlinks.tsx +5 -5
  10. package/app/components/CreateSpaceModal.tsx +3 -2
  11. package/app/components/DirPicker.tsx +1 -1
  12. package/app/components/DirView.tsx +2 -3
  13. package/app/components/EditorWrapper.tsx +3 -3
  14. package/app/components/FileTree.tsx +25 -10
  15. package/app/components/GuideCard.tsx +4 -4
  16. package/app/components/HomeContent.tsx +6 -11
  17. package/app/components/MarkdownView.tsx +2 -2
  18. package/app/components/OnboardingView.tsx +1 -1
  19. package/app/components/Panel.tsx +1 -1
  20. package/app/components/RightAgentDetailPanel.tsx +1 -1
  21. package/app/components/RightAskPanel.tsx +1 -1
  22. package/app/components/SearchModal.tsx +10 -2
  23. package/app/components/SidebarLayout.tsx +35 -10
  24. package/app/components/ThemeToggle.tsx +1 -1
  25. package/app/components/agents/AgentDetailContent.tsx +454 -59
  26. package/app/components/agents/AgentsContentPage.tsx +70 -5
  27. package/app/components/agents/AgentsMcpSection.tsx +474 -159
  28. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  29. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  30. package/app/components/agents/AgentsSkillsSection.tsx +739 -121
  31. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  32. package/app/components/agents/agents-content-model.ts +292 -10
  33. package/app/components/ask/AskContent.tsx +34 -5
  34. package/app/components/ask/FileChip.tsx +1 -0
  35. package/app/components/ask/MentionPopover.tsx +13 -1
  36. package/app/components/ask/MessageList.tsx +5 -7
  37. package/app/components/ask/ToolCallBlock.tsx +4 -4
  38. package/app/components/changes/ChangesBanner.tsx +1 -2
  39. package/app/components/echo/EchoHero.tsx +10 -24
  40. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  41. package/app/components/echo/EchoPageSections.tsx +13 -9
  42. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  43. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  44. package/app/components/explore/ExploreContent.tsx +3 -7
  45. package/app/components/explore/UseCaseCard.tsx +4 -15
  46. package/app/components/panels/AgentsPanel.tsx +12 -104
  47. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  48. package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
  49. package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
  50. package/app/components/panels/EchoPanel.tsx +8 -10
  51. package/app/components/panels/PanelNavRow.tsx +9 -2
  52. package/app/components/panels/PluginsPanel.tsx +2 -2
  53. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  54. package/app/components/renderers/agent-inspector/manifest.ts +3 -3
  55. package/app/components/renderers/todo/manifest.ts +1 -0
  56. package/app/components/settings/AiTab.tsx +3 -3
  57. package/app/components/settings/AppearanceTab.tsx +2 -2
  58. package/app/components/settings/KnowledgeTab.tsx +3 -3
  59. package/app/components/settings/McpAgentInstall.tsx +3 -6
  60. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  61. package/app/components/settings/McpSkillRow.tsx +2 -3
  62. package/app/components/settings/McpSkillsSection.tsx +2 -2
  63. package/app/components/settings/McpTab.tsx +12 -13
  64. package/app/components/settings/MonitoringTab.tsx +13 -13
  65. package/app/components/settings/PluginsTab.tsx +2 -2
  66. package/app/components/settings/Primitives.tsx +3 -4
  67. package/app/components/settings/SettingsContent.tsx +3 -3
  68. package/app/components/settings/SyncTab.tsx +11 -17
  69. package/app/components/settings/UpdateTab.tsx +18 -21
  70. package/app/components/settings/types.ts +14 -0
  71. package/app/components/setup/StepKB.tsx +1 -1
  72. package/app/hooks/useMcpData.tsx +4 -2
  73. package/app/hooks/useMention.ts +25 -8
  74. package/app/lib/agent/log.ts +15 -18
  75. package/app/lib/agent/prompt.ts +17 -29
  76. package/app/lib/agent/stream-consumer.ts +3 -0
  77. package/app/lib/agent/to-agent-messages.ts +6 -4
  78. package/app/lib/core/agent-audit-log.ts +280 -0
  79. package/app/lib/core/index.ts +11 -0
  80. package/app/lib/fs.ts +9 -0
  81. package/app/lib/i18n-en.ts +259 -33
  82. package/app/lib/i18n-zh.ts +258 -32
  83. package/app/lib/mcp-agents.ts +231 -2
  84. package/app/lib/types.ts +2 -0
  85. package/package.json +1 -1
  86. package/scripts/migrate-agent-audit-log.js +170 -0
@@ -1,89 +1,448 @@
1
1
  'use client';
2
2
 
3
- import { AlertTriangle, CheckCircle2, Wrench } from 'lucide-react';
3
+ import Link from 'next/link';
4
+ import { useMemo, useState } from 'react';
5
+ import {
6
+ AlertTriangle,
7
+ ArrowRight,
8
+ Cable,
9
+ ChevronDown,
10
+ Server,
11
+ Zap,
12
+ } from 'lucide-react';
13
+ import { cn } from '@/lib/utils';
14
+ import type { AgentInfo } from '@/components/settings/types';
4
15
  import type { AgentBuckets, RiskItem } from './agents-content-model';
16
+ import { resolveAgentStatus } from './agents-content-model';
17
+ import { AgentAvatar } from './AgentsPrimitives';
18
+
19
+ interface OverviewCopy {
20
+ connected: string;
21
+ detected: string;
22
+ notFound: string;
23
+ riskQueue: string;
24
+ usagePulse: string;
25
+ nextAction: string;
26
+ nextActionHint: string;
27
+ riskLevelError: string;
28
+ riskLevelWarn: string;
29
+ colAgent: string;
30
+ colStatus: string;
31
+ colMcp: string;
32
+ colSkills: string;
33
+ colMode: string;
34
+ colRuntime: string;
35
+ pulseMcp: string;
36
+ pulseTools: string;
37
+ mcpOffline: string;
38
+ toolsUnit: (n: number) => string;
39
+ enabledUnit: (n: number) => string;
40
+ agentCount: (n: number) => string;
41
+ runtimeActive: string;
42
+ [k: string]: unknown;
43
+ }
44
+
45
+ interface PulseCopy {
46
+ title: string;
47
+ healthy: string;
48
+ needsAttention: (n: number) => string;
49
+ connected: string;
50
+ detected: string;
51
+ notFound: string;
52
+ risk: string;
53
+ enabledSkills: string;
54
+ }
5
55
 
6
56
  export default function AgentsOverviewSection({
7
57
  copy,
8
58
  buckets,
9
59
  riskQueue,
10
- topSkillsLabel,
11
- failedAgentsLabel,
12
- topSkillsValue,
13
- failedAgentsValue,
60
+ mcpRunning,
61
+ mcpPort,
62
+ mcpToolCount,
63
+ enabledSkillCount,
64
+ allAgents,
65
+ pulseCopy,
14
66
  }: {
15
- copy: {
16
- connected: string;
17
- detected: string;
18
- notFound: string;
19
- riskQueue: string;
20
- noRisk: string;
21
- usagePulse: string;
22
- successRate7d: string;
23
- topSkills: string;
24
- failedAgents: string;
25
- na: string;
26
- };
67
+ copy: OverviewCopy;
27
68
  buckets: AgentBuckets;
28
69
  riskQueue: RiskItem[];
29
- topSkillsLabel: string;
30
- failedAgentsLabel: string;
31
- topSkillsValue: string;
32
- failedAgentsValue: string;
70
+ mcpRunning: boolean;
71
+ mcpPort: number | null;
72
+ mcpToolCount: number;
73
+ enabledSkillCount: number;
74
+ allAgents: AgentInfo[];
75
+ pulseCopy: PulseCopy;
33
76
  }) {
77
+ const allHealthy = riskQueue.length === 0 && mcpRunning;
78
+ const totalAgents = allAgents.length;
79
+ const [riskOpen, setRiskOpen] = useState(false);
80
+
81
+ const sortedAgents = useMemo(
82
+ () =>
83
+ [...allAgents].sort((a, b) => {
84
+ const rank = (x: AgentInfo) => (x.present && x.installed ? 0 : x.present ? 1 : 2);
85
+ return rank(a) - rank(b) || a.name.localeCompare(b.name);
86
+ }),
87
+ [allAgents],
88
+ );
89
+
34
90
  return (
35
- <div className="space-y-4">
36
- <section className="grid grid-cols-1 md:grid-cols-3 gap-3">
37
- <StatCard title={copy.connected} value={String(buckets.connected.length)} tone="ok" />
38
- <StatCard title={copy.detected} value={String(buckets.detected.length)} tone="warn" />
39
- <StatCard title={copy.notFound} value={String(buckets.notFound.length)} tone="warn" />
91
+ <div className="space-y-5">
92
+ {/* ═══════════ HERO STATS BAR ═══════════ */}
93
+ <section
94
+ className="rounded-xl border border-border bg-card overflow-hidden"
95
+ aria-label={pulseCopy.connected}
96
+ >
97
+ <div className="flex divide-x divide-border [&>*]:flex-1">
98
+ <StatCell
99
+ icon={<Zap size={14} aria-hidden="true" />}
100
+ label={pulseCopy.connected}
101
+ value={buckets.connected.length}
102
+ total={totalAgents}
103
+ tone="ok"
104
+ />
105
+ <StatCell
106
+ icon={<Cable size={14} aria-hidden="true" />}
107
+ label={pulseCopy.detected}
108
+ value={buckets.detected.length}
109
+ tone={buckets.detected.length > 0 ? 'warn' : 'muted'}
110
+ />
111
+ {buckets.notFound.length > 0 && (
112
+ <StatCell
113
+ icon={<AlertTriangle size={14} aria-hidden="true" />}
114
+ label={pulseCopy.notFound}
115
+ value={buckets.notFound.length}
116
+ tone="muted"
117
+ />
118
+ )}
119
+ <StatCell
120
+ icon={<Zap size={14} aria-hidden="true" />}
121
+ label={pulseCopy.enabledSkills}
122
+ value={enabledSkillCount}
123
+ tone="ok"
124
+ />
125
+ <StatCell
126
+ icon={<Server size={14} aria-hidden="true" />}
127
+ label={copy.pulseMcp as string}
128
+ value={mcpRunning ? `:${mcpPort}` : '—'}
129
+ tone={mcpRunning ? 'ok' : 'warn'}
130
+ />
131
+ </div>
40
132
  </section>
41
133
 
42
- <section className="rounded-lg border border-border bg-card p-4">
43
- <h2 className="text-sm font-medium text-foreground mb-3">{copy.riskQueue}</h2>
44
- {riskQueue.length === 0 ? (
45
- <p className="text-sm text-muted-foreground">{copy.noRisk}</p>
46
- ) : (
47
- <ul className="space-y-2">
48
- {riskQueue.map((risk) => (
49
- <li key={risk.id} className="flex items-start gap-2 text-sm">
50
- <AlertTriangle size={14} className={risk.severity === 'error' ? 'text-destructive mt-0.5' : 'text-[var(--amber)] mt-0.5'} />
51
- <span className="text-foreground">{risk.title}</span>
52
- </li>
53
- ))}
54
- </ul>
55
- )}
56
- </section>
134
+ {/* ═══════════ RISK CAPSULE ═══════════ */}
135
+ {riskQueue.length > 0 && (
136
+ <div>
137
+ <button
138
+ type="button"
139
+ onClick={() => setRiskOpen(!riskOpen)}
140
+ className="inline-flex items-center gap-1.5 rounded-full border border-amber-500/25 bg-amber-500/[0.06] px-3 py-1.5 text-sm font-medium text-[var(--amber)] transition-colors duration-150 hover:bg-amber-500/[0.12] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
141
+ aria-expanded={riskOpen}
142
+ >
143
+ <AlertTriangle size={13} className="shrink-0" aria-hidden="true" />
144
+ {copy.riskQueue}
145
+ <span className="tabular-nums text-2xs bg-[var(--amber-dim)] px-1.5 py-0.5 rounded-full select-none">
146
+ {riskQueue.length}
147
+ </span>
148
+ <ChevronDown
149
+ size={13}
150
+ className={cn('shrink-0 transition-transform duration-200', riskOpen && 'rotate-180')}
151
+ aria-hidden="true"
152
+ />
153
+ </button>
57
154
 
58
- <section className="rounded-lg border border-border bg-card p-4">
59
- <h2 className="text-sm font-medium text-foreground mb-3">{copy.usagePulse}</h2>
60
- <div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
61
- <InfoLine label={copy.successRate7d} value={copy.na} />
62
- <InfoLine label={topSkillsLabel || copy.topSkills} value={topSkillsValue} />
63
- <InfoLine label={failedAgentsLabel || copy.failedAgents} value={failedAgentsValue} />
155
+ <div
156
+ className={cn(
157
+ 'grid transition-[grid-template-rows] duration-250 ease-out',
158
+ riskOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
159
+ )}
160
+ >
161
+ <div className="overflow-hidden">
162
+ <ul className="mt-3 space-y-2" role="list">
163
+ {riskQueue.map((risk, i) => (
164
+ <li
165
+ key={risk.id}
166
+ className={`flex items-center gap-3 px-3 py-2.5 rounded-lg border text-sm ${
167
+ risk.severity === 'error'
168
+ ? 'border-destructive/20 bg-destructive/[0.03]'
169
+ : 'border-amber-500/15 bg-background'
170
+ }`}
171
+ style={{ animationDelay: `${i * 50}ms` }}
172
+ >
173
+ <span
174
+ className={`w-1.5 h-1.5 rounded-full shrink-0 ${
175
+ risk.severity === 'error' ? 'bg-destructive' : 'bg-[var(--amber)]'
176
+ }`}
177
+ aria-hidden="true"
178
+ />
179
+ <span className="text-foreground flex-1 min-w-0">{risk.title}</span>
180
+ <span
181
+ className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 select-none ${
182
+ risk.severity === 'error'
183
+ ? 'bg-destructive/10 text-destructive'
184
+ : 'bg-[var(--amber-dim)] text-[var(--amber)]'
185
+ }`}
186
+ >
187
+ {risk.severity === 'error' ? copy.riskLevelError : copy.riskLevelWarn}
188
+ </span>
189
+ </li>
190
+ ))}
191
+ </ul>
192
+ </div>
193
+ </div>
64
194
  </div>
65
- </section>
195
+ )}
196
+
197
+ {/* ═══════════ QUICK NAVIGATION ═══════════ */}
198
+ <nav className="grid grid-cols-1 md:grid-cols-2 gap-3" aria-label="Quick navigation">
199
+ <QuickNavCard
200
+ href="/agents?tab=mcp"
201
+ icon={<Server size={18} aria-hidden="true" />}
202
+ title="MCP"
203
+ stat={mcpRunning ? copy.toolsUnit(mcpToolCount) : copy.mcpOffline}
204
+ statTone={mcpRunning ? 'ok' : 'warn'}
205
+ description={copy.nextActionHint as string}
206
+ />
207
+ <QuickNavCard
208
+ href="/agents?tab=skills"
209
+ icon={<Zap size={18} aria-hidden="true" />}
210
+ title="Skills"
211
+ stat={copy.enabledUnit(enabledSkillCount)}
212
+ statTone="ok"
213
+ description={`${pulseCopy.enabledSkills}: ${enabledSkillCount}`}
214
+ />
215
+ </nav>
216
+
217
+ {/* ═══════════ AGENT CARDS ═══════════ */}
218
+ {sortedAgents.length > 0 ? (
219
+ <section aria-label={copy.usagePulse}>
220
+ <div className="flex items-center justify-between mb-3">
221
+ <h2 className="text-sm font-medium text-foreground">{copy.usagePulse}</h2>
222
+ <span className="text-2xs text-muted-foreground tabular-nums select-none">
223
+ {copy.agentCount(totalAgents)}
224
+ </span>
225
+ </div>
226
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
227
+ {sortedAgents.map((agent, i) => (
228
+ <AgentCard key={agent.key} agent={agent} copy={copy} index={i} />
229
+ ))}
230
+ </div>
231
+ </section>
232
+ ) : (
233
+ <section
234
+ className="rounded-xl border border-dashed border-border bg-card/50 p-10 text-center"
235
+ aria-label={copy.usagePulse}
236
+ >
237
+ <div className="w-12 h-12 rounded-full bg-muted/60 flex items-center justify-center mx-auto mb-3">
238
+ <Cable size={20} className="text-muted-foreground" aria-hidden="true" />
239
+ </div>
240
+ <p className="text-sm text-muted-foreground leading-relaxed max-w-xs mx-auto">
241
+ {copy.nextActionHint as string}
242
+ </p>
243
+ </section>
244
+ )}
66
245
  </div>
67
246
  );
68
247
  }
69
248
 
70
- function StatCard({ title, value, tone }: { title: string; value: string; tone: 'ok' | 'warn' }) {
249
+ /* ────────── Stat Cell ────────── */
250
+
251
+ function StatCell({
252
+ icon,
253
+ label,
254
+ value,
255
+ total,
256
+ tone,
257
+ }: {
258
+ icon: React.ReactNode;
259
+ label: string;
260
+ value: number | string;
261
+ total?: number;
262
+ tone: 'ok' | 'warn' | 'muted';
263
+ }) {
264
+ const textColor =
265
+ tone === 'ok'
266
+ ? 'text-foreground'
267
+ : tone === 'warn'
268
+ ? 'text-amber-600 dark:text-amber-400'
269
+ : 'text-muted-foreground';
270
+ const iconColor =
271
+ tone === 'ok'
272
+ ? 'text-muted-foreground'
273
+ : tone === 'warn'
274
+ ? 'text-amber-500/70'
275
+ : 'text-muted-foreground/50';
276
+
71
277
  return (
72
- <div className="rounded-lg border border-border bg-card p-4">
73
- <div className="flex items-center justify-between mb-2">
74
- <p className="text-xs text-muted-foreground">{title}</p>
75
- {tone === 'ok' ? <CheckCircle2 size={14} className="text-success" /> : <Wrench size={14} className="text-[var(--amber)]" />}
278
+ <div
279
+ className="px-3 py-3.5 text-center hover:bg-muted/20 transition-colors duration-100 group/stat"
280
+ role="group"
281
+ aria-label={`${label}: ${value}${total !== undefined ? `/${total}` : ''}`}
282
+ >
283
+ <div className={`flex items-center justify-center gap-1.5 mb-1.5 ${iconColor} group-hover/stat:text-foreground transition-colors duration-100`}>
284
+ {icon}
285
+ <span className="text-2xs text-muted-foreground truncate">{label}</span>
76
286
  </div>
77
- <p className="text-xl font-semibold text-foreground">{value}</p>
287
+ <p className={`text-lg font-semibold tabular-nums leading-none ${textColor}`}>
288
+ {value}
289
+ {total !== undefined && (
290
+ <span className="text-xs font-normal text-muted-foreground ml-0.5">/{total}</span>
291
+ )}
292
+ </p>
78
293
  </div>
79
294
  );
80
295
  }
81
296
 
82
- function InfoLine({ label, value }: { label: string; value: string }) {
297
+ /* ────────── Quick Nav Card ────────── */
298
+
299
+ function QuickNavCard({
300
+ href,
301
+ icon,
302
+ title,
303
+ stat,
304
+ statTone,
305
+ description,
306
+ }: {
307
+ href: string;
308
+ icon: React.ReactNode;
309
+ title: string;
310
+ stat: string;
311
+ statTone: 'ok' | 'warn';
312
+ description: string;
313
+ }) {
83
314
  return (
84
- <div className="rounded-md border border-border px-3 py-2">
85
- <p className="text-2xs text-muted-foreground mb-1">{label}</p>
86
- <p className="text-sm text-foreground truncate">{value}</p>
87
- </div>
315
+ <Link
316
+ href={href}
317
+ className="group rounded-xl border border-border bg-card p-4 flex items-start gap-3.5
318
+ hover:border-[var(--amber)]/30 hover:bg-muted/20 hover:shadow-sm
319
+ active:scale-[0.99]
320
+ transition-all duration-150
321
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
322
+ >
323
+ <div className="shrink-0 w-10 h-10 rounded-lg bg-muted/60 flex items-center justify-center text-muted-foreground group-hover:text-[var(--amber)] group-hover:bg-[var(--amber-dim)] transition-colors duration-150">
324
+ {icon}
325
+ </div>
326
+ <div className="flex-1 min-w-0">
327
+ <div className="flex items-center gap-2 mb-1">
328
+ <span className="text-sm font-semibold text-foreground">{title}</span>
329
+ <span
330
+ className={`text-2xs px-1.5 py-0.5 rounded font-medium select-none ${
331
+ statTone === 'ok'
332
+ ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
333
+ : 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
334
+ }`}
335
+ >
336
+ {stat}
337
+ </span>
338
+ </div>
339
+ <p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">{description}</p>
340
+ </div>
341
+ <ArrowRight
342
+ size={14}
343
+ className="shrink-0 mt-1.5 text-muted-foreground/30 group-hover:text-[var(--amber)] group-hover:translate-x-0.5 transition-all duration-150"
344
+ aria-hidden="true"
345
+ />
346
+ </Link>
347
+ );
348
+ }
349
+
350
+ /* ────────── Agent Card ────────── */
351
+
352
+ function AgentCard({
353
+ agent,
354
+ copy,
355
+ index,
356
+ }: {
357
+ agent: AgentInfo;
358
+ copy: OverviewCopy;
359
+ index: number;
360
+ }) {
361
+ const status = resolveAgentStatus(agent);
362
+ const mcpCount = agent.configuredMcpServerCount ?? agent.configuredMcpServers?.length ?? 0;
363
+ const skillCount = agent.installedSkillCount ?? agent.installedSkillNames?.length ?? 0;
364
+ const hasRuntime = agent.runtimeConversationSignal || agent.runtimeUsageSignal;
365
+
366
+ const statusLabel =
367
+ status === 'connected' ? copy.connected : status === 'detected' ? copy.detected : copy.notFound;
368
+ const statusColor =
369
+ status === 'connected'
370
+ ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
371
+ : status === 'detected'
372
+ ? 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
373
+ : 'bg-zinc-500/10 text-zinc-500';
374
+
375
+ return (
376
+ <Link
377
+ href={`/agents/${encodeURIComponent(agent.key)}`}
378
+ className="group rounded-xl border border-border bg-card p-3.5
379
+ hover:border-[var(--amber)]/30 hover:shadow-sm
380
+ active:scale-[0.98]
381
+ transition-all duration-150 animate-in
382
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
383
+ style={{ animationDelay: `${Math.min(index * 30, 300)}ms` }}
384
+ >
385
+ {/* Top row: avatar + name + status */}
386
+ <div className="flex items-center gap-2.5 mb-3">
387
+ <AgentAvatar name={agent.name} status={status} size="sm" />
388
+ <div className="flex-1 min-w-0">
389
+ <span className="text-sm font-medium text-foreground block truncate group-hover:text-[var(--amber)] transition-colors duration-150">
390
+ {agent.name}
391
+ </span>
392
+ {agent.transport && status === 'connected' && (
393
+ <span className="text-2xs text-muted-foreground font-mono">{agent.transport}</span>
394
+ )}
395
+ </div>
396
+ <span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 select-none ${statusColor}`}>
397
+ {statusLabel}
398
+ </span>
399
+ </div>
400
+
401
+ {/* Metrics row */}
402
+ <div className="flex items-center gap-0 pt-2.5 border-t border-border/50">
403
+ <MetricChip icon={<Server size={11} aria-hidden="true" />} value={mcpCount} label={copy.colMcp as string} />
404
+ <span className="text-border mx-2 select-none" aria-hidden="true">·</span>
405
+ <MetricChip icon={<Zap size={11} aria-hidden="true" />} value={skillCount} label={copy.colSkills as string} />
406
+ {agent.skillMode && (
407
+ <>
408
+ <span className="text-border mx-2 select-none" aria-hidden="true">·</span>
409
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground truncate select-none">
410
+ {agent.skillMode}
411
+ </span>
412
+ </>
413
+ )}
414
+ <span className="flex-1 min-w-[4px]" />
415
+ {hasRuntime && (
416
+ <span
417
+ className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
418
+ title={copy.runtimeActive}
419
+ >
420
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" aria-hidden="true" />
421
+ <span className="text-2xs font-medium">{copy.runtimeActive}</span>
422
+ </span>
423
+ )}
424
+ </div>
425
+ </Link>
426
+ );
427
+ }
428
+
429
+ /* ────────── Metric Chip ────────── */
430
+
431
+ function MetricChip({
432
+ icon,
433
+ value,
434
+ label,
435
+ }: {
436
+ icon: React.ReactNode;
437
+ value: number;
438
+ label: string;
439
+ }) {
440
+ return (
441
+ <span className="inline-flex items-center gap-1" title={label} aria-label={`${label}: ${value}`}>
442
+ <span className="text-muted-foreground/60">{icon}</span>
443
+ <span className={`tabular-nums text-xs ${value > 0 ? 'text-foreground font-medium' : 'text-muted-foreground/50'}`}>
444
+ {value}
445
+ </span>
446
+ </span>
88
447
  );
89
448
  }