@geminilight/mindos 0.6.27 → 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 (67) 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 +51 -6
  15. package/app/components/agents/AgentsContentPage.tsx +24 -6
  16. package/app/components/agents/AgentsOverviewSection.tsx +11 -0
  17. package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
  18. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  19. package/app/components/agents/agents-content-model.ts +2 -2
  20. package/app/components/ask/AskContent.tsx +8 -0
  21. package/app/components/help/HelpContent.tsx +74 -18
  22. package/app/components/panels/AgentsPanel.tsx +1 -0
  23. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  24. package/app/components/panels/AgentsPanelAgentListRow.tsx +10 -1
  25. package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
  26. package/app/components/panels/EchoPanel.tsx +5 -1
  27. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  28. package/app/components/settings/KnowledgeTab.tsx +3 -6
  29. package/app/components/settings/McpSkillsSection.tsx +4 -5
  30. package/app/components/settings/McpTab.tsx +6 -8
  31. package/app/components/setup/StepSecurity.tsx +4 -5
  32. package/app/components/setup/index.tsx +5 -11
  33. package/app/components/ui/Toaster.tsx +39 -0
  34. package/app/hooks/useA2aRegistry.ts +6 -1
  35. package/app/hooks/useAcpDetection.ts +65 -0
  36. package/app/hooks/useAcpRegistry.ts +51 -0
  37. package/app/hooks/useDelegationHistory.ts +49 -0
  38. package/app/lib/a2a/client.ts +49 -5
  39. package/app/lib/a2a/orchestrator.ts +0 -1
  40. package/app/lib/a2a/task-handler.ts +4 -4
  41. package/app/lib/a2a/types.ts +15 -0
  42. package/app/lib/acp/acp-tools.ts +93 -0
  43. package/app/lib/acp/bridge.ts +138 -0
  44. package/app/lib/acp/index.ts +24 -0
  45. package/app/lib/acp/registry.ts +135 -0
  46. package/app/lib/acp/session.ts +264 -0
  47. package/app/lib/acp/subprocess.ts +209 -0
  48. package/app/lib/acp/types.ts +136 -0
  49. package/app/lib/agent/tools.ts +2 -1
  50. package/app/lib/i18n/_core.ts +22 -0
  51. package/app/lib/i18n/index.ts +35 -0
  52. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  53. package/app/lib/i18n/modules/common.ts +71 -0
  54. package/app/lib/i18n/modules/features.ts +153 -0
  55. package/app/lib/i18n/modules/knowledge.ts +425 -0
  56. package/app/lib/i18n/modules/navigation.ts +151 -0
  57. package/app/lib/i18n/modules/onboarding.ts +523 -0
  58. package/app/lib/i18n/modules/panels.ts +1052 -0
  59. package/app/lib/i18n/modules/settings.ts +585 -0
  60. package/app/lib/i18n-en.ts +2 -1518
  61. package/app/lib/i18n-zh.ts +2 -1542
  62. package/app/lib/i18n.ts +3 -6
  63. package/app/lib/toast.ts +79 -0
  64. package/bin/cli.js +25 -25
  65. package/bin/commands/file.js +29 -2
  66. package/bin/commands/space.js +249 -91
  67. 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
 
@@ -200,11 +200,15 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
200
200
 
201
201
  // Focus and init session when becoming visible (edge-triggered for panel, level-triggered for modal)
202
202
  const prevVisibleRef = useRef(false);
203
+ const prevFileRef = useRef(currentFile);
203
204
  useEffect(() => {
204
205
  const justOpened = variant === 'panel'
205
206
  ? (visible && !prevVisibleRef.current) // panel: edge detection
206
207
  : visible; // modal: level detection (reset every open)
207
208
 
209
+ // Detect file change while panel is already open
210
+ const fileChanged = visible && prevVisibleRef.current && currentFile !== prevFileRef.current;
211
+
208
212
  if (justOpened) {
209
213
  setTimeout(() => inputRef.current?.focus(), 50);
210
214
  void session.initSessions();
@@ -216,11 +220,15 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
216
220
  slash.resetSlash();
217
221
  setSelectedSkill(null);
218
222
  setShowHistory(false);
223
+ } else if (fileChanged) {
224
+ // Update attached file context to match new file (don't reset session/messages)
225
+ setAttachedFiles(currentFile ? [currentFile] : []);
219
226
  } else if (!visible && variant === 'modal') {
220
227
  // Modal: abort streaming on close
221
228
  abortRef.current?.abort();
222
229
  }
223
230
  prevVisibleRef.current = visible;
231
+ prevFileRef.current = currentFile;
224
232
  // eslint-disable-next-line react-hooks/exhaustive-deps
225
233
  }, [visible, currentFile]);
226
234