@geminilight/mindos 0.5.64 → 0.5.65
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/README.md +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +6 -11
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +1 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +35 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +70 -5
- package/app/components/agents/AgentsMcpSection.tsx +474 -159
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +739 -121
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +292 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +1 -2
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +12 -104
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
- package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +3 -3
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +2 -2
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +4 -2
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +9 -0
- package/app/lib/i18n-en.ts +259 -33
- package/app/lib/i18n-zh.ts +258 -32
- package/app/lib/mcp-agents.ts +231 -2
- package/app/lib/types.ts +2 -0
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
|
@@ -1,89 +1,448 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
}
|