@geminilight/mindos 0.6.28 → 0.6.29
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/app/app/api/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/detect/route.ts +91 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +55 -0
- package/app/app/layout.tsx +2 -0
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +19 -0
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/agents/AgentDetailContent.tsx +3 -5
- package/app/components/agents/AgentsContentPage.tsx +21 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- package/app/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpDetection.ts +65 -0
- package/app/hooks/useAcpRegistry.ts +51 -0
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +93 -0
- package/app/lib/acp/bridge.ts +138 -0
- package/app/lib/acp/index.ts +24 -0
- package/app/lib/acp/registry.ts +135 -0
- package/app/lib/acp/session.ts +264 -0
- package/app/lib/acp/subprocess.ts +209 -0
- package/app/lib/acp/types.ts +136 -0
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +425 -0
- package/app/lib/i18n/modules/navigation.ts +151 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1052 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- package/app/lib/toast.ts +79 -0
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Clock, Globe, Loader2, Network, RefreshCw, Trash2, Wifi, WifiOff, Zap } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import type { RemoteAgent, DelegationRecord } from '@/lib/a2a/types';
|
|
7
|
+
import type { AcpRegistryEntry } from '@/lib/acp/types';
|
|
8
|
+
import { useDelegationHistory } from '@/hooks/useDelegationHistory';
|
|
9
|
+
import { useAcpRegistry } from '@/hooks/useAcpRegistry';
|
|
10
|
+
import { useAcpDetection } from '@/hooks/useAcpDetection';
|
|
11
|
+
import { openAskModal } from '@/hooks/useAskModal';
|
|
12
|
+
import DiscoverAgentModal from './DiscoverAgentModal';
|
|
13
|
+
|
|
14
|
+
interface AgentsPanelA2aTabProps {
|
|
15
|
+
agents: RemoteAgent[];
|
|
16
|
+
discovering: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
onDiscover: (url: string) => Promise<RemoteAgent | null>;
|
|
19
|
+
onRemove: (id: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function AgentsPanelA2aTab({
|
|
23
|
+
agents,
|
|
24
|
+
discovering,
|
|
25
|
+
error,
|
|
26
|
+
onDiscover,
|
|
27
|
+
onRemove,
|
|
28
|
+
}: AgentsPanelA2aTabProps) {
|
|
29
|
+
const { t } = useLocale();
|
|
30
|
+
const p = t.panels.agents;
|
|
31
|
+
const [showModal, setShowModal] = useState(false);
|
|
32
|
+
const { delegations } = useDelegationHistory(true);
|
|
33
|
+
const acp = useAcpRegistry();
|
|
34
|
+
|
|
35
|
+
const isEmpty = agents.length === 0 && !acp.loading && acp.agents.length === 0;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-5">
|
|
39
|
+
{/* Header + Discover button */}
|
|
40
|
+
<div className="flex items-center justify-between">
|
|
41
|
+
<h2 className="text-sm font-medium text-foreground">{p.a2aTabTitle}</h2>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => setShowModal(true)}
|
|
45
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-foreground text-background hover:bg-foreground/90 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
46
|
+
>
|
|
47
|
+
<Globe size={12} />
|
|
48
|
+
{p.a2aDiscover}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Unified empty state when both A2A and ACP are empty */}
|
|
53
|
+
{isEmpty ? (
|
|
54
|
+
<NetworkEmptyState
|
|
55
|
+
onDiscover={() => setShowModal(true)}
|
|
56
|
+
onBrowseRegistry={acp.retry}
|
|
57
|
+
/>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
{/* Remote A2A agent list */}
|
|
61
|
+
{agents.length === 0 ? (
|
|
62
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-gradient-to-b from-card/80 to-card/40 p-8 text-center">
|
|
63
|
+
<div className="w-12 h-12 rounded-2xl bg-muted/40 flex items-center justify-center mx-auto mb-3">
|
|
64
|
+
<Globe size={20} className="text-muted-foreground/50" aria-hidden="true" />
|
|
65
|
+
</div>
|
|
66
|
+
<p className="text-sm font-medium text-muted-foreground mb-1">{p.a2aTabEmpty}</p>
|
|
67
|
+
<p className="text-xs text-muted-foreground/70 leading-relaxed max-w-xs mx-auto">
|
|
68
|
+
{p.a2aTabEmptyHint}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
{agents.map((agent) => (
|
|
74
|
+
<RemoteAgentRow key={agent.id} agent={agent} onRemove={onRemove} removeCopy={p.a2aRemoveAgent} skillsCopy={p.a2aSkills} />
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* ACP Registry section */}
|
|
80
|
+
<AcpRegistrySection />
|
|
81
|
+
|
|
82
|
+
{/* Recent Delegations */}
|
|
83
|
+
<DelegationHistorySection delegations={delegations} />
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
<DiscoverAgentModal
|
|
88
|
+
open={showModal}
|
|
89
|
+
onClose={() => setShowModal(false)}
|
|
90
|
+
onDiscover={onDiscover}
|
|
91
|
+
discovering={discovering}
|
|
92
|
+
error={error}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ────────── Network Empty State ────────── */
|
|
99
|
+
|
|
100
|
+
function NetworkEmptyState({
|
|
101
|
+
onDiscover,
|
|
102
|
+
onBrowseRegistry,
|
|
103
|
+
}: {
|
|
104
|
+
onDiscover: () => void;
|
|
105
|
+
onBrowseRegistry: () => void;
|
|
106
|
+
}) {
|
|
107
|
+
const { t } = useLocale();
|
|
108
|
+
const p = t.panels.agents;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-gradient-to-b from-card/80 to-card/40 p-12 text-center">
|
|
112
|
+
<div className="w-14 h-14 rounded-2xl bg-muted/40 flex items-center justify-center mx-auto mb-4">
|
|
113
|
+
<Network size={22} className="text-muted-foreground/50" aria-hidden="true" />
|
|
114
|
+
</div>
|
|
115
|
+
<p className="text-sm font-medium text-foreground mb-1">{p.networkEmptyTitle}</p>
|
|
116
|
+
<p className="text-xs text-muted-foreground/70 leading-relaxed max-w-sm mx-auto mb-5">
|
|
117
|
+
{p.networkEmptyDesc}
|
|
118
|
+
</p>
|
|
119
|
+
<div className="flex items-center justify-center gap-2.5">
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onDiscover}
|
|
123
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-foreground text-background hover:bg-foreground/90 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
124
|
+
>
|
|
125
|
+
<Globe size={12} />
|
|
126
|
+
{p.networkDiscoverBtn}
|
|
127
|
+
</button>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={onBrowseRegistry}
|
|
131
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
132
|
+
>
|
|
133
|
+
<Network size={12} />
|
|
134
|
+
{p.networkBrowseBtn}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ────────── ACP Registry Section ────────── */
|
|
142
|
+
|
|
143
|
+
function AcpRegistrySection() {
|
|
144
|
+
const { t } = useLocale();
|
|
145
|
+
const p = t.panels.agents;
|
|
146
|
+
const acp = useAcpRegistry();
|
|
147
|
+
const detection = useAcpDetection();
|
|
148
|
+
|
|
149
|
+
if (acp.loading) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
153
|
+
{p.acpSectionTitle}
|
|
154
|
+
</h3>
|
|
155
|
+
<div className="flex items-center justify-center py-6 gap-2">
|
|
156
|
+
<Loader2 size={14} className="animate-spin text-muted-foreground" />
|
|
157
|
+
<span className="text-xs text-muted-foreground">{p.acpLoading}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (acp.error) {
|
|
164
|
+
return (
|
|
165
|
+
<div className="space-y-2">
|
|
166
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
167
|
+
{p.acpSectionTitle}
|
|
168
|
+
</h3>
|
|
169
|
+
<div className="rounded-lg border border-border/60 bg-card/80 p-4 text-center">
|
|
170
|
+
<p className="text-xs text-muted-foreground mb-2">{p.acpLoadFailed}</p>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={acp.retry}
|
|
174
|
+
className="text-xs font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
175
|
+
>
|
|
176
|
+
{p.acpRetry}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (acp.agents.length === 0) return null;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="space-y-2">
|
|
187
|
+
<div className="flex items-center justify-between">
|
|
188
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
189
|
+
{p.acpSectionTitle}
|
|
190
|
+
</h3>
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => detection.refresh()}
|
|
195
|
+
disabled={detection.loading}
|
|
196
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-2xs font-medium rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
197
|
+
>
|
|
198
|
+
<RefreshCw size={10} className={detection.loading ? 'animate-spin' : ''} />
|
|
199
|
+
{p.acpScan}
|
|
200
|
+
</button>
|
|
201
|
+
<span className="text-2xs text-muted-foreground/60">
|
|
202
|
+
{p.acpSectionDesc(acp.agents.length)}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="space-y-1.5">
|
|
207
|
+
{acp.agents.map((agent) => {
|
|
208
|
+
const installed = detection.installedAgents.find((d) => d.id === agent.id);
|
|
209
|
+
const notInstalled = detection.notInstalledAgents.find((d) => d.id === agent.id);
|
|
210
|
+
return (
|
|
211
|
+
<AcpAgentRow
|
|
212
|
+
key={agent.id}
|
|
213
|
+
agent={agent}
|
|
214
|
+
installed={installed ?? null}
|
|
215
|
+
installCmd={notInstalled?.installCmd ?? null}
|
|
216
|
+
detectionDone={!detection.loading}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ────────── ACP Agent Row ────────── */
|
|
226
|
+
|
|
227
|
+
const TRANSPORT_STYLES: Record<string, string> = {
|
|
228
|
+
npx: 'bg-[var(--amber)]/15 text-[var(--amber)]',
|
|
229
|
+
binary: 'bg-muted text-muted-foreground',
|
|
230
|
+
uvx: 'bg-[var(--success)]/15 text-[var(--success)]',
|
|
231
|
+
stdio: 'bg-muted text-muted-foreground',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function AcpAgentRow({ agent, installed, installCmd, detectionDone }: {
|
|
235
|
+
agent: AcpRegistryEntry;
|
|
236
|
+
installed: { id: string; name: string; binaryPath: string } | null;
|
|
237
|
+
installCmd: string | null;
|
|
238
|
+
detectionDone: boolean;
|
|
239
|
+
}) {
|
|
240
|
+
const { t } = useLocale();
|
|
241
|
+
const p = t.panels.agents;
|
|
242
|
+
const transportLabels: Record<string, string> = {
|
|
243
|
+
npx: p.acpTransportNpx,
|
|
244
|
+
binary: p.acpTransportBinary,
|
|
245
|
+
uvx: p.acpTransportUvx,
|
|
246
|
+
stdio: p.acpTransportStdio,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const isReady = !!installed;
|
|
250
|
+
|
|
251
|
+
const handleUse = () => {
|
|
252
|
+
openAskModal(`Use ${agent.name} to help me with `);
|
|
253
|
+
window.dispatchEvent(
|
|
254
|
+
new CustomEvent('mindos:ask-with-agent', {
|
|
255
|
+
detail: { agentId: agent.id, agentName: agent.name },
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div className="group rounded-xl border border-border bg-card p-3 hover:border-border/80 transition-all duration-150">
|
|
262
|
+
<div className="flex items-center gap-2.5">
|
|
263
|
+
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
|
264
|
+
<Network size={14} className="text-muted-foreground" />
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex-1 min-w-0">
|
|
267
|
+
<p className="text-sm font-medium text-foreground truncate">{agent.name}</p>
|
|
268
|
+
{agent.description && (
|
|
269
|
+
<p className="text-2xs text-muted-foreground truncate">{agent.description}</p>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${TRANSPORT_STYLES[agent.transport] ?? TRANSPORT_STYLES.stdio}`}>
|
|
273
|
+
{transportLabels[agent.transport] ?? agent.transport}
|
|
274
|
+
</span>
|
|
275
|
+
{detectionDone && (
|
|
276
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${
|
|
277
|
+
isReady
|
|
278
|
+
? 'bg-[var(--success)]/15 text-[var(--success)]'
|
|
279
|
+
: 'bg-muted text-muted-foreground/60'
|
|
280
|
+
}`}>
|
|
281
|
+
{isReady ? p.acpReady : p.acpNotInstalled}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
disabled={!isReady}
|
|
287
|
+
onClick={handleUse}
|
|
288
|
+
className={`inline-flex items-center gap-1 px-2 py-1 text-2xs font-medium rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
289
|
+
isReady
|
|
290
|
+
? 'border-[var(--amber)] text-[var(--amber)] hover:bg-[var(--amber)]/10 cursor-pointer'
|
|
291
|
+
: 'border-border text-muted-foreground/50 cursor-not-allowed'
|
|
292
|
+
}`}
|
|
293
|
+
title={
|
|
294
|
+
isReady
|
|
295
|
+
? undefined
|
|
296
|
+
: installCmd
|
|
297
|
+
? p.acpInstallHint(installCmd)
|
|
298
|
+
: p.acpComingSoon
|
|
299
|
+
}
|
|
300
|
+
>
|
|
301
|
+
{p.acpUseAgent}
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ────────── Delegation History Section ────────── */
|
|
309
|
+
|
|
310
|
+
function DelegationHistorySection({ delegations }: { delegations: DelegationRecord[] }) {
|
|
311
|
+
const { t } = useLocale();
|
|
312
|
+
const p = t.panels.agents;
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="space-y-2">
|
|
316
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
317
|
+
{p.a2aDelegations}
|
|
318
|
+
</h3>
|
|
319
|
+
{delegations.length === 0 ? (
|
|
320
|
+
<p className="text-xs text-muted-foreground/70 py-3">{p.a2aDelegationsEmpty}</p>
|
|
321
|
+
) : (
|
|
322
|
+
<div className="space-y-1.5">
|
|
323
|
+
{delegations.map((d) => (
|
|
324
|
+
<DelegationRow key={d.id} record={d} />
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ────────── Delegation Row ────────── */
|
|
333
|
+
|
|
334
|
+
const STATUS_STYLES: Record<DelegationRecord['status'], string> = {
|
|
335
|
+
pending: 'bg-muted text-muted-foreground',
|
|
336
|
+
completed: 'bg-[var(--success)]/15 text-[var(--success)]',
|
|
337
|
+
failed: 'bg-[var(--error)]/15 text-[var(--error)]',
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function DelegationRow({ record }: { record: DelegationRecord }) {
|
|
341
|
+
const { t } = useLocale();
|
|
342
|
+
const p = t.panels.agents;
|
|
343
|
+
const statusLabels: Record<DelegationRecord['status'], string> = {
|
|
344
|
+
pending: p.a2aDelegationPending,
|
|
345
|
+
completed: p.a2aDelegationCompleted,
|
|
346
|
+
failed: p.a2aDelegationFailed,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const duration = record.completedAt
|
|
350
|
+
? formatDuration(new Date(record.startedAt), new Date(record.completedAt))
|
|
351
|
+
: null;
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className="rounded-lg border border-border/60 bg-card/80 px-3 py-2.5 flex items-center gap-2.5">
|
|
355
|
+
<div className="flex-1 min-w-0">
|
|
356
|
+
<p className="text-xs font-medium text-foreground truncate">{record.agentName}</p>
|
|
357
|
+
<p className="text-2xs text-muted-foreground truncate" title={record.message}>
|
|
358
|
+
{record.message.length > 60 ? record.message.slice(0, 60) + '...' : record.message}
|
|
359
|
+
</p>
|
|
360
|
+
</div>
|
|
361
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${STATUS_STYLES[record.status]}`}>
|
|
362
|
+
{statusLabels[record.status]}
|
|
363
|
+
</span>
|
|
364
|
+
{duration && (
|
|
365
|
+
<span className="text-2xs text-muted-foreground/60 shrink-0 flex items-center gap-0.5">
|
|
366
|
+
<Clock size={10} aria-hidden="true" />
|
|
367
|
+
{duration}
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatDuration(start: Date, end: Date): string {
|
|
375
|
+
const ms = end.getTime() - start.getTime();
|
|
376
|
+
if (ms < 1000) return `${ms}ms`;
|
|
377
|
+
const secs = Math.round(ms / 1000);
|
|
378
|
+
if (secs < 60) return `${secs}s`;
|
|
379
|
+
const mins = Math.floor(secs / 60);
|
|
380
|
+
const remSecs = secs % 60;
|
|
381
|
+
return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* ────────── Remote Agent Row ────────── */
|
|
385
|
+
|
|
386
|
+
function RemoteAgentRow({
|
|
387
|
+
agent,
|
|
388
|
+
onRemove,
|
|
389
|
+
removeCopy,
|
|
390
|
+
skillsCopy,
|
|
391
|
+
}: {
|
|
392
|
+
agent: RemoteAgent;
|
|
393
|
+
onRemove: (id: string) => void;
|
|
394
|
+
removeCopy: string;
|
|
395
|
+
skillsCopy: string;
|
|
396
|
+
}) {
|
|
397
|
+
const StatusIcon = agent.reachable ? Wifi : WifiOff;
|
|
398
|
+
const statusColor = agent.reachable
|
|
399
|
+
? 'text-[var(--success)]'
|
|
400
|
+
: 'text-muted-foreground/50';
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div className="group rounded-xl border border-border bg-card p-3.5 hover:border-[var(--amber)]/30 transition-all duration-150">
|
|
404
|
+
<div className="flex items-center gap-2.5">
|
|
405
|
+
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
|
406
|
+
<Globe size={14} className="text-muted-foreground" />
|
|
407
|
+
</div>
|
|
408
|
+
<div className="flex-1 min-w-0">
|
|
409
|
+
<p className="text-sm font-medium text-foreground truncate">{agent.card.name}</p>
|
|
410
|
+
<p className="text-2xs text-muted-foreground truncate">{agent.card.description}</p>
|
|
411
|
+
</div>
|
|
412
|
+
<StatusIcon size={13} className={statusColor} aria-hidden="true" />
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
onClick={() => onRemove(agent.id)}
|
|
416
|
+
className="p-1.5 rounded-md text-muted-foreground/50 hover:text-error hover:bg-error/10 transition-colors opacity-0 group-hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:opacity-100"
|
|
417
|
+
aria-label={removeCopy}
|
|
418
|
+
title={removeCopy}
|
|
419
|
+
>
|
|
420
|
+
<Trash2 size={12} />
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
{agent.card.skills.length > 0 && (
|
|
424
|
+
<div className="mt-2.5 pt-2 border-t border-border/40 flex items-center gap-1.5">
|
|
425
|
+
<Zap size={11} className="text-muted-foreground/60 shrink-0" aria-hidden="true" />
|
|
426
|
+
<span className="text-2xs text-muted-foreground">{skillsCopy}: {agent.card.skills.length}</span>
|
|
427
|
+
<div className="flex flex-wrap gap-1 ml-1">
|
|
428
|
+
{agent.card.skills.slice(0, 3).map((s) => (
|
|
429
|
+
<span
|
|
430
|
+
key={s.id}
|
|
431
|
+
className="text-2xs px-1.5 py-0.5 rounded bg-muted/80 text-muted-foreground border border-border/50"
|
|
432
|
+
title={s.description}
|
|
433
|
+
>
|
|
434
|
+
{s.name}
|
|
435
|
+
</span>
|
|
436
|
+
))}
|
|
437
|
+
{agent.card.skills.length > 3 && (
|
|
438
|
+
<span className="text-2xs text-muted-foreground/60">+{agent.card.skills.length - 3}</span>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
BookOpen,
|
|
6
6
|
Code2,
|
|
7
7
|
Copy,
|
|
8
|
-
Check,
|
|
9
8
|
FileText,
|
|
10
9
|
Loader2,
|
|
11
10
|
Plus,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
} from 'lucide-react';
|
|
20
19
|
import { apiFetch } from '@/lib/api';
|
|
21
20
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
21
|
+
import { toast } from '@/lib/toast';
|
|
22
22
|
import type { SkillInfo } from '@/components/settings/types';
|
|
23
23
|
import { Toggle } from '@/components/settings/Primitives';
|
|
24
24
|
import { AgentAvatar, ConfirmDialog } from './AgentsPrimitives';
|
|
@@ -187,7 +187,6 @@ export default function SkillDetailPopover({
|
|
|
187
187
|
const [nativeDesc, setNativeDesc] = useState<string>('');
|
|
188
188
|
const [loading, setLoading] = useState(false);
|
|
189
189
|
const [loadError, setLoadError] = useState(false);
|
|
190
|
-
const [copied, setCopied] = useState(false);
|
|
191
190
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
192
191
|
const [deleting, setDeleting] = useState(false);
|
|
193
192
|
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
@@ -228,7 +227,6 @@ export default function SkillDetailPopover({
|
|
|
228
227
|
setContent(null);
|
|
229
228
|
setNativeDesc('');
|
|
230
229
|
setLoadError(false);
|
|
231
|
-
setCopied(false);
|
|
232
230
|
setDeleteMsg(null);
|
|
233
231
|
setDeleting(false);
|
|
234
232
|
setToggleBusy(false);
|
|
@@ -250,10 +248,7 @@ export default function SkillDetailPopover({
|
|
|
250
248
|
const handleCopy = useCallback(async () => {
|
|
251
249
|
if (!content) return;
|
|
252
250
|
const ok = await copyToClipboard(content);
|
|
253
|
-
if (ok)
|
|
254
|
-
setCopied(true);
|
|
255
|
-
setTimeout(() => setCopied(false), 1500);
|
|
256
|
-
}
|
|
251
|
+
if (ok) toast.copy();
|
|
257
252
|
}, [content]);
|
|
258
253
|
|
|
259
254
|
const handleToggle = useCallback(async (enabled: boolean) => {
|
|
@@ -442,8 +437,8 @@ export default function SkillDetailPopover({
|
|
|
442
437
|
className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
|
|
443
438
|
aria-label={copy.copyContent}
|
|
444
439
|
>
|
|
445
|
-
|
|
446
|
-
{
|
|
440
|
+
<Copy size={11} />
|
|
441
|
+
{copy.copyContent}
|
|
447
442
|
</button>
|
|
448
443
|
)}
|
|
449
444
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentInfo, SkillInfo } from '@/components/settings/types';
|
|
2
2
|
|
|
3
|
-
export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills';
|
|
3
|
+
export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills' | 'a2a';
|
|
4
4
|
export type AgentResolvedStatus = 'connected' | 'detected' | 'notFound';
|
|
5
5
|
export type SkillCapability = 'research' | 'coding' | 'docs' | 'ops' | 'memory';
|
|
6
6
|
export type SkillSourceFilter = 'all' | 'builtin' | 'user';
|
|
@@ -24,7 +24,7 @@ export interface AgentBuckets {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function parseAgentsTab(tab: string | undefined): AgentsDashboardTab {
|
|
27
|
-
if (tab === 'mcp' || tab === 'skills') return tab;
|
|
27
|
+
if (tab === 'mcp' || tab === 'skills' || tab === 'a2a') return tab;
|
|
28
28
|
return 'overview';
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
4
4
|
import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
|
+
import { toast } from '@/lib/toast';
|
|
6
8
|
|
|
7
9
|
/* ── Collapsible Section ── */
|
|
8
10
|
function Section({ id, icon, title, defaultOpen = false, children }: {
|
|
@@ -56,15 +58,13 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
|
|
|
56
58
|
const [copied, setCopied] = useState(false);
|
|
57
59
|
|
|
58
60
|
const handleCopy = useCallback(() => {
|
|
59
|
-
const clean = text.replace(/^["
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
setCopied(true); // Reuse copied state to show error
|
|
67
|
-
setTimeout(() => setCopied(false), 2000);
|
|
61
|
+
const clean = text.replace(/^["“]|["”]$/g, '');
|
|
62
|
+
copyToClipboard(clean).then((ok) => {
|
|
63
|
+
if (ok) {
|
|
64
|
+
setCopied(true);
|
|
65
|
+
setTimeout(() => setCopied(false), 1500);
|
|
66
|
+
toast.copy();
|
|
67
|
+
}
|
|
68
68
|
});
|
|
69
69
|
}, [text]);
|
|
70
70
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react';
|
|
4
|
-
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy,
|
|
4
|
+
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy, Monitor, Globe } from 'lucide-react';
|
|
5
|
+
import { toast } from '@/lib/toast';
|
|
5
6
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
6
7
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
8
|
import type { AgentInfo, McpStatus } from '../settings/types';
|
|
@@ -48,7 +49,6 @@ export default function AgentsPanelAgentDetail({
|
|
|
48
49
|
const [installing, setInstalling] = useState(false);
|
|
49
50
|
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
50
51
|
const [transport, setTransport] = useState<'stdio' | 'http'>(() => agent.preferredTransport);
|
|
51
|
-
const [copied, setCopied] = useState(false);
|
|
52
52
|
|
|
53
53
|
const snippet = useMemo(() => {
|
|
54
54
|
if (agentStatus === 'notFound') return null;
|
|
@@ -70,10 +70,7 @@ export default function AgentsPanelAgentDetail({
|
|
|
70
70
|
const handleCopy = async () => {
|
|
71
71
|
if (!snippet) return;
|
|
72
72
|
const ok = await copyToClipboard(snippet.snippet);
|
|
73
|
-
if (ok)
|
|
74
|
-
setCopied(true);
|
|
75
|
-
setTimeout(() => setCopied(false), 2000);
|
|
76
|
-
}
|
|
73
|
+
if (ok) toast.copy();
|
|
77
74
|
};
|
|
78
75
|
|
|
79
76
|
const dot =
|
|
@@ -183,8 +180,8 @@ export default function AgentsPanelAgentDetail({
|
|
|
183
180
|
onClick={handleCopy}
|
|
184
181
|
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
185
182
|
>
|
|
186
|
-
|
|
187
|
-
{
|
|
183
|
+
<Copy size={14} />
|
|
184
|
+
{copy.copyConfig}
|
|
188
185
|
</button>
|
|
189
186
|
</>
|
|
190
187
|
)}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
4
|
+
import { Globe, LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
5
5
|
import { PanelNavRow } from './PanelNavRow';
|
|
6
6
|
|
|
7
7
|
type HubCopy = {
|
|
8
8
|
navOverview: string;
|
|
9
9
|
navMcp: string;
|
|
10
10
|
navSkills: string;
|
|
11
|
+
navNetwork: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function AgentsPanelHubNav({
|
|
@@ -43,6 +44,12 @@ export function AgentsPanelHubNav({
|
|
|
43
44
|
href="/agents?tab=skills"
|
|
44
45
|
active={inAgentsRoute && tab === 'skills'}
|
|
45
46
|
/>
|
|
47
|
+
<PanelNavRow
|
|
48
|
+
icon={<Globe size={14} className={inAgentsRoute && tab === 'a2a' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
49
|
+
title={copy.navNetwork}
|
|
50
|
+
href="/agents?tab=a2a"
|
|
51
|
+
active={inAgentsRoute && tab === 'a2a'}
|
|
52
|
+
/>
|
|
46
53
|
</div>
|
|
47
54
|
);
|
|
48
55
|
}
|
|
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
|
|
|
5
5
|
import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
|
|
6
6
|
import PanelHeader from './PanelHeader';
|
|
7
7
|
import { PanelNavRow } from './PanelNavRow';
|
|
8
|
+
import EchoSidebarStats from './EchoSidebarStats';
|
|
8
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
9
10
|
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
10
11
|
|
|
@@ -30,7 +31,7 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
30
31
|
return (
|
|
31
32
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
32
33
|
<PanelHeader title={e.title} maximized={maximized} onMaximize={onMaximize} />
|
|
33
|
-
<div className="flex-1 overflow-y-auto min-h-0">
|
|
34
|
+
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
|
34
35
|
<div className="flex flex-col gap-0.5 py-1.5">
|
|
35
36
|
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
36
37
|
const row = rowBySegment[segment];
|
|
@@ -41,6 +42,9 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
41
42
|
);
|
|
42
43
|
})}
|
|
43
44
|
</div>
|
|
45
|
+
<div className="mt-auto">
|
|
46
|
+
<EchoSidebarStats />
|
|
47
|
+
</div>
|
|
44
48
|
</div>
|
|
45
49
|
</div>
|
|
46
50
|
);
|