@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.
Files changed (64) hide show
  1. package/app/app/api/a2a/agents/route.ts +9 -0
  2. package/app/app/api/a2a/delegations/route.ts +9 -0
  3. package/app/app/api/a2a/discover/route.ts +2 -0
  4. package/app/app/api/a2a/route.ts +6 -6
  5. package/app/app/api/acp/detect/route.ts +91 -0
  6. package/app/app/api/acp/registry/route.ts +31 -0
  7. package/app/app/api/acp/session/route.ts +55 -0
  8. package/app/app/layout.tsx +2 -0
  9. package/app/components/DirView.tsx +64 -2
  10. package/app/components/FileTree.tsx +19 -0
  11. package/app/components/GuideCard.tsx +7 -17
  12. package/app/components/MarkdownView.tsx +2 -0
  13. package/app/components/SearchModal.tsx +234 -80
  14. package/app/components/agents/AgentDetailContent.tsx +3 -5
  15. package/app/components/agents/AgentsContentPage.tsx +21 -6
  16. package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
  17. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  18. package/app/components/agents/agents-content-model.ts +2 -2
  19. package/app/components/help/HelpContent.tsx +9 -9
  20. package/app/components/panels/AgentsPanel.tsx +1 -0
  21. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  22. package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
  23. package/app/components/panels/EchoPanel.tsx +5 -1
  24. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  25. package/app/components/settings/KnowledgeTab.tsx +3 -6
  26. package/app/components/settings/McpSkillsSection.tsx +4 -5
  27. package/app/components/settings/McpTab.tsx +6 -8
  28. package/app/components/setup/StepSecurity.tsx +4 -5
  29. package/app/components/setup/index.tsx +5 -11
  30. package/app/components/ui/Toaster.tsx +39 -0
  31. package/app/hooks/useA2aRegistry.ts +6 -1
  32. package/app/hooks/useAcpDetection.ts +65 -0
  33. package/app/hooks/useAcpRegistry.ts +51 -0
  34. package/app/hooks/useDelegationHistory.ts +49 -0
  35. package/app/lib/a2a/client.ts +49 -5
  36. package/app/lib/a2a/orchestrator.ts +0 -1
  37. package/app/lib/a2a/task-handler.ts +4 -4
  38. package/app/lib/a2a/types.ts +15 -0
  39. package/app/lib/acp/acp-tools.ts +93 -0
  40. package/app/lib/acp/bridge.ts +138 -0
  41. package/app/lib/acp/index.ts +24 -0
  42. package/app/lib/acp/registry.ts +135 -0
  43. package/app/lib/acp/session.ts +264 -0
  44. package/app/lib/acp/subprocess.ts +209 -0
  45. package/app/lib/acp/types.ts +136 -0
  46. package/app/lib/agent/tools.ts +2 -1
  47. package/app/lib/i18n/_core.ts +22 -0
  48. package/app/lib/i18n/index.ts +35 -0
  49. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  50. package/app/lib/i18n/modules/common.ts +71 -0
  51. package/app/lib/i18n/modules/features.ts +153 -0
  52. package/app/lib/i18n/modules/knowledge.ts +425 -0
  53. package/app/lib/i18n/modules/navigation.ts +151 -0
  54. package/app/lib/i18n/modules/onboarding.ts +523 -0
  55. package/app/lib/i18n/modules/panels.ts +1052 -0
  56. package/app/lib/i18n/modules/settings.ts +585 -0
  57. package/app/lib/i18n-en.ts +2 -1518
  58. package/app/lib/i18n-zh.ts +2 -1542
  59. package/app/lib/i18n.ts +3 -6
  60. package/app/lib/toast.ts +79 -0
  61. package/bin/cli.js +25 -25
  62. package/bin/commands/file.js +29 -2
  63. package/bin/commands/space.js +249 -91
  64. 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
- {copied ? <Check size={11} /> : <Copy size={11} />}
446
- {copied ? copy.copied : copy.copyContent}
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(/^[""]|[""]$/g, '');
60
- navigator.clipboard.writeText(clean).then(() => {
61
- setCopied(true);
62
- setTimeout(() => setCopied(false), 1500);
63
- }).catch((err) => {
64
- console.error('[HelpContent] Clipboard copy failed:', err);
65
- // Show error feedback in UI
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
 
@@ -70,6 +70,7 @@ export default function AgentsPanel({
70
70
  navOverview: p.navOverview,
71
71
  navMcp: p.navMcp,
72
72
  navSkills: p.navSkills,
73
+ navNetwork: p.navNetwork,
73
74
  };
74
75
 
75
76
  const hub = (
@@ -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, Check, Monitor, Globe } from 'lucide-react';
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
- {copied ? <Check size={14} /> : <Copy size={14} />}
187
- {copied ? copy.copied : copy.copyConfig}
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
  );