@geminilight/mindos 0.6.28 → 0.6.30

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 (113) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/a2a/agents/route.ts +9 -0
  3. package/app/app/api/a2a/delegations/route.ts +9 -0
  4. package/app/app/api/a2a/discover/route.ts +2 -0
  5. package/app/app/api/a2a/route.ts +6 -6
  6. package/app/app/api/acp/config/route.ts +82 -0
  7. package/app/app/api/acp/detect/route.ts +114 -0
  8. package/app/app/api/acp/install/route.ts +51 -0
  9. package/app/app/api/acp/registry/route.ts +31 -0
  10. package/app/app/api/acp/session/route.ts +185 -0
  11. package/app/app/api/ask/route.ts +116 -13
  12. package/app/app/api/workflows/route.ts +156 -0
  13. package/app/app/layout.tsx +2 -0
  14. package/app/app/page.tsx +7 -2
  15. package/app/components/ActivityBar.tsx +12 -4
  16. package/app/components/AskModal.tsx +4 -1
  17. package/app/components/DirView.tsx +64 -2
  18. package/app/components/FileTree.tsx +40 -10
  19. package/app/components/GuideCard.tsx +7 -17
  20. package/app/components/HomeContent.tsx +1 -0
  21. package/app/components/MarkdownView.tsx +2 -0
  22. package/app/components/Panel.tsx +1 -0
  23. package/app/components/RightAskPanel.tsx +5 -1
  24. package/app/components/SearchModal.tsx +234 -80
  25. package/app/components/SidebarLayout.tsx +6 -0
  26. package/app/components/agents/AgentDetailContent.tsx +266 -52
  27. package/app/components/agents/AgentsContentPage.tsx +32 -6
  28. package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
  29. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  30. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  31. package/app/components/agents/agents-content-model.ts +2 -2
  32. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  33. package/app/components/ask/AskContent.tsx +197 -239
  34. package/app/components/ask/FileChip.tsx +82 -17
  35. package/app/components/ask/MentionPopover.tsx +21 -3
  36. package/app/components/ask/MessageList.tsx +30 -9
  37. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  38. package/app/components/help/HelpContent.tsx +9 -9
  39. package/app/components/panels/AgentsPanel.tsx +2 -0
  40. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  41. package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
  42. package/app/components/panels/EchoPanel.tsx +5 -1
  43. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  44. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  45. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  46. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  49. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  50. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  51. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  52. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  53. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  54. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  55. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  56. package/app/components/settings/KnowledgeTab.tsx +3 -6
  57. package/app/components/settings/McpSkillsSection.tsx +4 -5
  58. package/app/components/settings/McpTab.tsx +6 -8
  59. package/app/components/setup/StepSecurity.tsx +4 -5
  60. package/app/components/setup/index.tsx +5 -11
  61. package/app/components/ui/Toaster.tsx +39 -0
  62. package/app/hooks/useA2aRegistry.ts +6 -1
  63. package/app/hooks/useAcpConfig.ts +96 -0
  64. package/app/hooks/useAcpDetection.ts +120 -0
  65. package/app/hooks/useAcpRegistry.ts +86 -0
  66. package/app/hooks/useAskModal.ts +12 -5
  67. package/app/hooks/useAskPanel.ts +8 -5
  68. package/app/hooks/useAskSession.ts +19 -2
  69. package/app/hooks/useDelegationHistory.ts +49 -0
  70. package/app/hooks/useImageUpload.ts +152 -0
  71. package/app/lib/a2a/client.ts +49 -5
  72. package/app/lib/a2a/orchestrator.ts +0 -1
  73. package/app/lib/a2a/task-handler.ts +4 -4
  74. package/app/lib/a2a/types.ts +15 -0
  75. package/app/lib/acp/acp-tools.ts +95 -0
  76. package/app/lib/acp/agent-descriptors.ts +274 -0
  77. package/app/lib/acp/bridge.ts +144 -0
  78. package/app/lib/acp/index.ts +40 -0
  79. package/app/lib/acp/registry.ts +202 -0
  80. package/app/lib/acp/session.ts +717 -0
  81. package/app/lib/acp/subprocess.ts +495 -0
  82. package/app/lib/acp/types.ts +274 -0
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/to-agent-messages.ts +25 -2
  85. package/app/lib/agent/tools.ts +2 -1
  86. package/app/lib/i18n/_core.ts +22 -0
  87. package/app/lib/i18n/index.ts +35 -0
  88. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  89. package/app/lib/i18n/modules/common.ts +71 -0
  90. package/app/lib/i18n/modules/features.ts +153 -0
  91. package/app/lib/i18n/modules/knowledge.ts +429 -0
  92. package/app/lib/i18n/modules/navigation.ts +153 -0
  93. package/app/lib/i18n/modules/onboarding.ts +523 -0
  94. package/app/lib/i18n/modules/panels.ts +1196 -0
  95. package/app/lib/i18n/modules/settings.ts +585 -0
  96. package/app/lib/i18n-en.ts +2 -1518
  97. package/app/lib/i18n-zh.ts +2 -1542
  98. package/app/lib/i18n.ts +3 -6
  99. package/app/lib/pi-integration/skills.ts +21 -6
  100. package/app/lib/renderers/index.ts +2 -2
  101. package/app/lib/settings.ts +10 -0
  102. package/app/lib/toast.ts +79 -0
  103. package/app/lib/types.ts +12 -1
  104. package/app/next-env.d.ts +1 -1
  105. package/app/package.json +3 -1
  106. package/bin/cli.js +25 -25
  107. package/bin/commands/file.js +29 -2
  108. package/bin/commands/space.js +249 -91
  109. package/package.json +1 -1
  110. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  111. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  112. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  113. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -0,0 +1,684 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { Check, ChevronDown, ChevronUp, Clock, Code2, Download, Globe, Loader2, MessageSquare, Network, RefreshCw, RotateCcw, Save, Settings2, Trash2, Wifi, WifiOff, Wrench, Zap } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { useAcpConfig } from '@/hooks/useAcpConfig';
7
+ import type { RemoteAgent, DelegationRecord } from '@/lib/a2a/types';
8
+ import type { AcpRegistryEntry } from '@/lib/acp/types';
9
+ import { useDelegationHistory } from '@/hooks/useDelegationHistory';
10
+ import { useAcpRegistry } from '@/hooks/useAcpRegistry';
11
+ import { useAcpDetection } from '@/hooks/useAcpDetection';
12
+ import { openAskModal } from '@/hooks/useAskModal';
13
+ import DiscoverAgentModal from './DiscoverAgentModal';
14
+
15
+ interface AgentsPanelA2aTabProps {
16
+ agents: RemoteAgent[];
17
+ discovering: boolean;
18
+ error: string | null;
19
+ onDiscover: (url: string) => Promise<RemoteAgent | null>;
20
+ onRemove: (id: string) => void;
21
+ }
22
+
23
+ export default function AgentsPanelA2aTab({
24
+ agents,
25
+ discovering,
26
+ error,
27
+ onDiscover,
28
+ onRemove,
29
+ }: AgentsPanelA2aTabProps) {
30
+ const { t } = useLocale();
31
+ const p = t.panels.agents;
32
+ const [showModal, setShowModal] = useState(false);
33
+ const { delegations } = useDelegationHistory(true);
34
+ const acp = useAcpRegistry();
35
+
36
+ const isEmpty = agents.length === 0 && !acp.loading && acp.agents.length === 0;
37
+
38
+ return (
39
+ <div className="space-y-5">
40
+ {/* Header + Discover button */}
41
+ <div className="flex items-center justify-between">
42
+ <h2 className="text-sm font-medium text-foreground">{p.a2aTabTitle}</h2>
43
+ <button
44
+ type="button"
45
+ onClick={() => setShowModal(true)}
46
+ 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"
47
+ >
48
+ <Globe size={12} />
49
+ {p.a2aDiscover}
50
+ </button>
51
+ </div>
52
+
53
+ {/* Unified empty state when both A2A and ACP are empty */}
54
+ {isEmpty ? (
55
+ <NetworkEmptyState
56
+ onDiscover={() => setShowModal(true)}
57
+ onBrowseRegistry={acp.retry}
58
+ />
59
+ ) : (
60
+ <>
61
+ {/* Remote A2A agent list */}
62
+ {agents.length === 0 ? (
63
+ <div className="rounded-xl border border-dashed border-border/60 bg-gradient-to-b from-card/80 to-card/40 p-8 text-center">
64
+ <div className="w-12 h-12 rounded-2xl bg-muted/40 flex items-center justify-center mx-auto mb-3">
65
+ <Globe size={20} className="text-muted-foreground/50" aria-hidden="true" />
66
+ </div>
67
+ <p className="text-sm font-medium text-muted-foreground mb-1">{p.a2aTabEmpty}</p>
68
+ <p className="text-xs text-muted-foreground/70 leading-relaxed max-w-xs mx-auto">
69
+ {p.a2aTabEmptyHint}
70
+ </p>
71
+ </div>
72
+ ) : (
73
+ <div className="space-y-2">
74
+ {agents.map((agent) => (
75
+ <RemoteAgentRow key={agent.id} agent={agent} onRemove={onRemove} removeCopy={p.a2aRemoveAgent} skillsCopy={p.a2aSkills} />
76
+ ))}
77
+ </div>
78
+ )}
79
+
80
+ {/* ACP Registry section */}
81
+ <AcpRegistrySection />
82
+
83
+ {/* Recent Delegations */}
84
+ <DelegationHistorySection delegations={delegations} />
85
+ </>
86
+ )}
87
+
88
+ <DiscoverAgentModal
89
+ open={showModal}
90
+ onClose={() => setShowModal(false)}
91
+ onDiscover={onDiscover}
92
+ discovering={discovering}
93
+ error={error}
94
+ />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ /* ────────── Network Empty State ────────── */
100
+
101
+ function NetworkEmptyState({
102
+ onDiscover,
103
+ onBrowseRegistry,
104
+ }: {
105
+ onDiscover: () => void;
106
+ onBrowseRegistry: () => void;
107
+ }) {
108
+ const { t } = useLocale();
109
+ const p = t.panels.agents;
110
+
111
+ return (
112
+ <div className="rounded-xl border border-dashed border-border/60 bg-gradient-to-b from-card/80 to-card/40 p-12 text-center">
113
+ <div className="w-14 h-14 rounded-2xl bg-muted/40 flex items-center justify-center mx-auto mb-4">
114
+ <Network size={22} className="text-muted-foreground/50" aria-hidden="true" />
115
+ </div>
116
+ <p className="text-sm font-medium text-foreground mb-1">{p.networkEmptyTitle}</p>
117
+ <p className="text-xs text-muted-foreground/70 leading-relaxed max-w-sm mx-auto mb-5">
118
+ {p.networkEmptyDesc}
119
+ </p>
120
+ <div className="flex items-center justify-center gap-2.5">
121
+ <button
122
+ type="button"
123
+ onClick={onDiscover}
124
+ 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"
125
+ >
126
+ <Globe size={12} />
127
+ {p.networkDiscoverBtn}
128
+ </button>
129
+ <button
130
+ type="button"
131
+ onClick={onBrowseRegistry}
132
+ 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"
133
+ >
134
+ <Network size={12} />
135
+ {p.networkBrowseBtn}
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ /* ────────── Quick Actions ────────── */
143
+
144
+ interface QuickAction {
145
+ labelKey: 'acpQuickReview' | 'acpQuickFix' | 'acpQuickExplain';
146
+ icon: typeof Code2;
147
+ promptSuffix: string;
148
+ }
149
+
150
+ const QUICK_ACTIONS: QuickAction[] = [
151
+ { labelKey: 'acpQuickReview', icon: Code2, promptSuffix: 'review the code in this project' },
152
+ { labelKey: 'acpQuickFix', icon: Wrench, promptSuffix: 'find and fix bugs in this project' },
153
+ { labelKey: 'acpQuickExplain', icon: MessageSquare, promptSuffix: 'explain the structure of this project' },
154
+ ];
155
+
156
+ /* ────────── ACP Registry Section ────────── */
157
+
158
+ function AcpRegistrySection() {
159
+ const { t } = useLocale();
160
+ const p = t.panels.agents;
161
+ const acp = useAcpRegistry();
162
+ const detection = useAcpDetection();
163
+ const acpConfig = useAcpConfig();
164
+
165
+ if (acp.loading) {
166
+ return (
167
+ <div className="space-y-2">
168
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
169
+ {p.acpSectionTitle}
170
+ </h3>
171
+ <div className="flex items-center justify-center py-6 gap-2">
172
+ <Loader2 size={14} className="animate-spin text-muted-foreground" />
173
+ <span className="text-xs text-muted-foreground">{p.acpLoading}</span>
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ if (acp.error) {
180
+ return (
181
+ <div className="space-y-2">
182
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
183
+ {p.acpSectionTitle}
184
+ </h3>
185
+ <div className="rounded-lg border border-border/60 bg-card/80 p-4 text-center">
186
+ <p className="text-xs text-muted-foreground mb-2">{p.acpLoadFailed}</p>
187
+ <button
188
+ type="button"
189
+ onClick={acp.retry}
190
+ className="text-xs font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
191
+ >
192
+ {p.acpRetry}
193
+ </button>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ if (acp.agents.length === 0) return null;
200
+
201
+ // Separate installed and not-installed agents
202
+ const installedAgents: { agent: AcpRegistryEntry; info: { id: string; name: string; binaryPath: string } }[] = [];
203
+ const notInstalledAgents: { agent: AcpRegistryEntry; installCmd: string | null; packageName: string | null }[] = [];
204
+
205
+ for (const agent of acp.agents) {
206
+ const installed = detection.installedAgents.find((d) => d.id === agent.id);
207
+ if (installed) {
208
+ installedAgents.push({ agent, info: installed });
209
+ } else {
210
+ const notInstalled = detection.notInstalledAgents.find((d) => d.id === agent.id);
211
+ notInstalledAgents.push({
212
+ agent,
213
+ installCmd: notInstalled?.installCmd ?? null,
214
+ packageName: notInstalled?.packageName ?? agent.packageName ?? null,
215
+ });
216
+ }
217
+ }
218
+
219
+ return (
220
+ <div className="space-y-3">
221
+ <div className="flex items-center justify-between">
222
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
223
+ {p.acpSectionTitle}
224
+ </h3>
225
+ <div className="flex items-center gap-2">
226
+ <button
227
+ type="button"
228
+ onClick={() => detection.refresh()}
229
+ disabled={detection.loading}
230
+ 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"
231
+ >
232
+ <RefreshCw size={10} className={detection.loading ? 'animate-spin' : ''} />
233
+ {p.acpScan}
234
+ </button>
235
+ <span className="text-2xs text-muted-foreground/60">
236
+ {p.acpSectionDesc(acp.agents.length)}
237
+ </span>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Installed agents — prominent cards */}
242
+ {installedAgents.length > 0 && (
243
+ <div className="space-y-2">
244
+ {installedAgents.map(({ agent, info }) => (
245
+ <AcpAgentCard
246
+ key={agent.id}
247
+ agent={agent}
248
+ installed={info}
249
+ installCmd={null}
250
+ packageName={null}
251
+ detectionDone={!detection.loading}
252
+ onInstalled={detection.refresh}
253
+ acpConfig={acpConfig}
254
+ />
255
+ ))}
256
+ </div>
257
+ )}
258
+
259
+ {/* Not-installed agents — compact rows grouped at bottom */}
260
+ {notInstalledAgents.length > 0 && (
261
+ <div className="space-y-1">
262
+ {notInstalledAgents.map(({ agent, installCmd, packageName }) => (
263
+ <AcpAgentCompactRow
264
+ key={agent.id}
265
+ agent={agent}
266
+ installCmd={installCmd}
267
+ packageName={packageName}
268
+ detectionDone={!detection.loading}
269
+ onInstalled={detection.refresh}
270
+ />
271
+ ))}
272
+ </div>
273
+ )}
274
+ </div>
275
+ );
276
+ }
277
+
278
+ /* ────────── ACP Agent Card (Installed — prominent) ────────── */
279
+
280
+ const TRANSPORT_STYLES: Record<string, string> = {
281
+ npx: 'bg-[var(--amber)]/15 text-[var(--amber)]',
282
+ binary: 'bg-muted text-muted-foreground',
283
+ uvx: 'bg-[var(--success)]/15 text-[var(--success)]',
284
+ stdio: 'bg-muted text-muted-foreground',
285
+ };
286
+
287
+ function AcpAgentCard({ agent, installed, detectionDone, acpConfig }: {
288
+ agent: AcpRegistryEntry;
289
+ installed: { id: string; name: string; binaryPath: string; resolvedCommand?: { cmd: string; args: string[]; source: string } } | null;
290
+ installCmd: string | null;
291
+ packageName: string | null;
292
+ detectionDone: boolean;
293
+ onInstalled: () => void;
294
+ acpConfig: ReturnType<typeof useAcpConfig>;
295
+ }) {
296
+ const { t } = useLocale();
297
+ const p = t.panels.agents;
298
+ const [configOpen, setConfigOpen] = useState(false);
299
+ const [editCmd, setEditCmd] = useState('');
300
+ const [editArgs, setEditArgs] = useState('');
301
+ const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle');
302
+ const transportLabels: Record<string, string> = {
303
+ npx: p.acpTransportNpx,
304
+ binary: p.acpTransportBinary,
305
+ uvx: p.acpTransportUvx,
306
+ stdio: p.acpTransportStdio,
307
+ };
308
+
309
+ const isReady = !!installed;
310
+ const resolved = installed?.resolvedCommand;
311
+ const sourceLabel = resolved?.source === 'user-override' ? p.acpConfigSourceUser
312
+ : resolved?.source === 'descriptor' ? p.acpConfigSourceBuiltin
313
+ : p.acpConfigSourceRegistry;
314
+
315
+ const handleToggleConfig = useCallback(() => {
316
+ if (!configOpen && resolved) {
317
+ // Pre-fill with current resolved values
318
+ setEditCmd(resolved.cmd);
319
+ setEditArgs(resolved.args.join(' '));
320
+ }
321
+ setConfigOpen(v => !v);
322
+ setSaveState('idle');
323
+ }, [configOpen, resolved]);
324
+
325
+ const handleSave = useCallback(async () => {
326
+ const args = editArgs.trim() ? editArgs.trim().split(/\s+/) : [];
327
+ const ok = await acpConfig.save(agent.id, {
328
+ command: editCmd.trim() || undefined,
329
+ args: args.length > 0 ? args : undefined,
330
+ });
331
+ if (ok) {
332
+ setSaveState('saved');
333
+ setTimeout(() => setSaveState('idle'), 2000);
334
+ }
335
+ }, [acpConfig, agent.id, editCmd, editArgs]);
336
+
337
+ const handleReset = useCallback(async () => {
338
+ await acpConfig.reset(agent.id);
339
+ setSaveState('idle');
340
+ setConfigOpen(false);
341
+ }, [acpConfig, agent.id]);
342
+
343
+ const handleUse = () => {
344
+ openAskModal('', 'user', { id: agent.id, name: agent.name });
345
+ };
346
+
347
+ const handleQuickAction = (action: QuickAction) => {
348
+ openAskModal(action.promptSuffix, 'user', { id: agent.id, name: agent.name });
349
+ };
350
+
351
+ return (
352
+ <div className="rounded-xl border border-[var(--amber)]/20 bg-card p-3 hover:border-[var(--amber)]/40 transition-all duration-150">
353
+ {/* Header row */}
354
+ <div className="flex items-center gap-2.5">
355
+ <div className="w-8 h-8 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
356
+ <Network size={14} className="text-[var(--amber)]" />
357
+ </div>
358
+ <div className="flex-1 min-w-0">
359
+ <div className="flex items-center gap-1.5">
360
+ <p className="text-sm font-medium text-foreground truncate">{agent.name}</p>
361
+ {agent.version && (
362
+ <span className="text-2xs text-muted-foreground/60 shrink-0">v{agent.version}</span>
363
+ )}
364
+ </div>
365
+ {agent.description && (
366
+ <p className="text-2xs text-muted-foreground truncate">{agent.description}</p>
367
+ )}
368
+ </div>
369
+ <span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${TRANSPORT_STYLES[agent.transport] ?? TRANSPORT_STYLES.stdio}`}>
370
+ {transportLabels[agent.transport] ?? agent.transport}
371
+ </span>
372
+ {detectionDone && (
373
+ <span className="text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 bg-[var(--success)]/15 text-[var(--success)]">
374
+ {p.acpReady}
375
+ </span>
376
+ )}
377
+ <button
378
+ type="button"
379
+ onClick={handleToggleConfig}
380
+ className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
381
+ title={p.acpConfigToggle}
382
+ aria-expanded={configOpen}
383
+ >
384
+ <Settings2 size={13} />
385
+ </button>
386
+ <button
387
+ type="button"
388
+ onClick={handleUse}
389
+ className="inline-flex items-center gap-1 px-2.5 py-1 text-2xs font-medium rounded-md border border-[var(--amber)] text-[var(--amber)] hover:bg-[var(--amber)]/10 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer"
390
+ >
391
+ <Zap size={10} />
392
+ {p.acpUseAgent}
393
+ </button>
394
+ </div>
395
+
396
+ {/* Collapsible config section */}
397
+ {configOpen && resolved && (
398
+ <div className="mt-2.5 pt-2.5 border-t border-border/40 space-y-2">
399
+ {/* Command */}
400
+ <div className="flex items-center gap-2">
401
+ <label className="text-2xs text-muted-foreground w-12 shrink-0">{p.acpConfigCommand}</label>
402
+ <input
403
+ type="text"
404
+ value={editCmd}
405
+ onChange={e => { setEditCmd(e.target.value); setSaveState('idle'); }}
406
+ className="flex-1 rounded-md border border-border bg-background text-foreground text-xs font-mono px-2 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
407
+ placeholder={resolved.cmd}
408
+ />
409
+ </div>
410
+ {/* Args */}
411
+ <div className="flex items-center gap-2">
412
+ <label className="text-2xs text-muted-foreground w-12 shrink-0">{p.acpConfigArgs}</label>
413
+ <input
414
+ type="text"
415
+ value={editArgs}
416
+ onChange={e => { setEditArgs(e.target.value); setSaveState('idle'); }}
417
+ className="flex-1 rounded-md border border-border bg-background text-foreground text-xs font-mono px-2 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
418
+ placeholder={resolved.args.join(' ')}
419
+ />
420
+ </div>
421
+ {/* Status row */}
422
+ <div className="flex items-center gap-2 text-2xs text-muted-foreground">
423
+ <span>{p.acpConfigSource}:</span>
424
+ <span className={`px-1.5 py-0.5 rounded font-medium ${
425
+ resolved.source === 'user-override' ? 'bg-[var(--amber)]/15 text-[var(--amber)]'
426
+ : 'bg-muted text-muted-foreground'
427
+ }`}>{sourceLabel}</span>
428
+ <span className="text-muted-foreground/50">|</span>
429
+ <span>{p.acpConfigPath}: <span className="font-mono">{installed?.binaryPath}</span></span>
430
+ </div>
431
+ {/* Action buttons */}
432
+ <div className="flex items-center justify-end gap-2 pt-1">
433
+ {acpConfig.configs[agent.id] && (
434
+ <button
435
+ type="button"
436
+ onClick={handleReset}
437
+ 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"
438
+ >
439
+ <RotateCcw size={10} />
440
+ {p.acpConfigReset}
441
+ </button>
442
+ )}
443
+ <button
444
+ type="button"
445
+ onClick={handleSave}
446
+ disabled={acpConfig.saving}
447
+ className="inline-flex items-center gap-1 px-2.5 py-1 text-2xs font-medium rounded-md bg-[var(--amber)] text-[var(--amber-foreground)] hover:bg-[var(--amber)]/90 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
448
+ >
449
+ {saveState === 'saved' ? <><Check size={10} /> {p.acpConfigSaved}</> : <><Save size={10} /> {p.acpConfigSave}</>}
450
+ </button>
451
+ </div>
452
+ </div>
453
+ )}
454
+
455
+ {/* Quick action chips */}
456
+ {isReady && !configOpen && (
457
+ <div className="mt-2.5 pt-2 border-t border-border/40 flex items-center gap-1.5 flex-wrap">
458
+ {QUICK_ACTIONS.map((action) => {
459
+ const Icon = action.icon;
460
+ return (
461
+ <button
462
+ key={action.labelKey}
463
+ type="button"
464
+ onClick={() => handleQuickAction(action)}
465
+ className="inline-flex items-center gap-1 px-2 py-0.5 text-2xs font-medium rounded-md bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground border border-border/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
466
+ >
467
+ <Icon size={10} />
468
+ {p[action.labelKey]}
469
+ </button>
470
+ );
471
+ })}
472
+ </div>
473
+ )}
474
+ </div>
475
+ );
476
+ }
477
+
478
+ /* ────────── ACP Agent Compact Row (Not Installed — subtle) ────────── */
479
+
480
+ function AcpAgentCompactRow({ agent, installCmd, packageName, detectionDone, onInstalled }: {
481
+ agent: AcpRegistryEntry;
482
+ installCmd: string | null;
483
+ packageName: string | null;
484
+ detectionDone: boolean;
485
+ onInstalled: () => void;
486
+ }) {
487
+ const { t } = useLocale();
488
+ const p = t.panels.agents;
489
+ const [installState, setInstallState] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
490
+
491
+ const handleInstall = async () => {
492
+ if (!packageName || installState === 'installing') return;
493
+ setInstallState('installing');
494
+ try {
495
+ const res = await fetch('/api/acp/install', {
496
+ method: 'POST',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ agentId: agent.id, packageName }),
499
+ });
500
+ if (!res.ok) {
501
+ setInstallState('error');
502
+ return;
503
+ }
504
+ // Wait a bit for npm install to complete, then re-detect
505
+ await new Promise((r) => setTimeout(r, 8000));
506
+ onInstalled();
507
+ setInstallState('done');
508
+ } catch {
509
+ setInstallState('error');
510
+ }
511
+ };
512
+
513
+ return (
514
+ <div className="group rounded-lg border border-border/60 bg-card/60 px-3 py-2 hover:border-border transition-all duration-150">
515
+ <div className="flex items-center gap-2">
516
+ <div className="w-6 h-6 rounded-md bg-muted/40 flex items-center justify-center shrink-0">
517
+ <Network size={11} className="text-muted-foreground/60" />
518
+ </div>
519
+ <div className="flex-1 min-w-0">
520
+ <p className="text-xs text-muted-foreground truncate">{agent.name}</p>
521
+ </div>
522
+ {detectionDone && (
523
+ <span className="text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 bg-muted text-muted-foreground/60">
524
+ {p.acpNotInstalled}
525
+ </span>
526
+ )}
527
+ {detectionDone && packageName && (
528
+ <button
529
+ type="button"
530
+ disabled={installState === 'installing'}
531
+ onClick={handleInstall}
532
+ className="inline-flex items-center gap-1 px-2 py-0.5 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"
533
+ title={installCmd ? p.acpInstallHint(installCmd) : undefined}
534
+ >
535
+ {installState === 'installing' ? (
536
+ <><Loader2 size={10} className="animate-spin" /> {p.acpInstalling}</>
537
+ ) : (
538
+ <><Download size={10} /> {p.acpInstall}</>
539
+ )}
540
+ </button>
541
+ )}
542
+ </div>
543
+ </div>
544
+ );
545
+ }
546
+
547
+ /* ────────── Delegation History Section ────────── */
548
+
549
+ function DelegationHistorySection({ delegations }: { delegations: DelegationRecord[] }) {
550
+ const { t } = useLocale();
551
+ const p = t.panels.agents;
552
+
553
+ return (
554
+ <div className="space-y-2">
555
+ <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
556
+ {p.a2aDelegations}
557
+ </h3>
558
+ {delegations.length === 0 ? (
559
+ <p className="text-xs text-muted-foreground/70 py-3">{p.a2aDelegationsEmpty}</p>
560
+ ) : (
561
+ <div className="space-y-1.5">
562
+ {delegations.map((d) => (
563
+ <DelegationRow key={d.id} record={d} />
564
+ ))}
565
+ </div>
566
+ )}
567
+ </div>
568
+ );
569
+ }
570
+
571
+ /* ────────── Delegation Row ────────── */
572
+
573
+ const STATUS_STYLES: Record<DelegationRecord['status'], string> = {
574
+ pending: 'bg-muted text-muted-foreground',
575
+ completed: 'bg-[var(--success)]/15 text-[var(--success)]',
576
+ failed: 'bg-[var(--error)]/15 text-[var(--error)]',
577
+ };
578
+
579
+ function DelegationRow({ record }: { record: DelegationRecord }) {
580
+ const { t } = useLocale();
581
+ const p = t.panels.agents;
582
+ const statusLabels: Record<DelegationRecord['status'], string> = {
583
+ pending: p.a2aDelegationPending,
584
+ completed: p.a2aDelegationCompleted,
585
+ failed: p.a2aDelegationFailed,
586
+ };
587
+
588
+ const duration = record.completedAt
589
+ ? formatDuration(new Date(record.startedAt), new Date(record.completedAt))
590
+ : null;
591
+
592
+ return (
593
+ <div className="rounded-lg border border-border/60 bg-card/80 px-3 py-2.5 flex items-center gap-2.5">
594
+ <div className="flex-1 min-w-0">
595
+ <p className="text-xs font-medium text-foreground truncate">{record.agentName}</p>
596
+ <p className="text-2xs text-muted-foreground truncate" title={record.message}>
597
+ {record.message.length > 60 ? record.message.slice(0, 60) + '...' : record.message}
598
+ </p>
599
+ </div>
600
+ <span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${STATUS_STYLES[record.status]}`}>
601
+ {statusLabels[record.status]}
602
+ </span>
603
+ {duration && (
604
+ <span className="text-2xs text-muted-foreground/60 shrink-0 flex items-center gap-0.5">
605
+ <Clock size={10} aria-hidden="true" />
606
+ {duration}
607
+ </span>
608
+ )}
609
+ </div>
610
+ );
611
+ }
612
+
613
+ function formatDuration(start: Date, end: Date): string {
614
+ const ms = end.getTime() - start.getTime();
615
+ if (ms < 1000) return `${ms}ms`;
616
+ const secs = Math.round(ms / 1000);
617
+ if (secs < 60) return `${secs}s`;
618
+ const mins = Math.floor(secs / 60);
619
+ const remSecs = secs % 60;
620
+ return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`;
621
+ }
622
+
623
+ /* ────────── Remote Agent Row ────────── */
624
+
625
+ function RemoteAgentRow({
626
+ agent,
627
+ onRemove,
628
+ removeCopy,
629
+ skillsCopy,
630
+ }: {
631
+ agent: RemoteAgent;
632
+ onRemove: (id: string) => void;
633
+ removeCopy: string;
634
+ skillsCopy: string;
635
+ }) {
636
+ const StatusIcon = agent.reachable ? Wifi : WifiOff;
637
+ const statusColor = agent.reachable
638
+ ? 'text-[var(--success)]'
639
+ : 'text-muted-foreground/50';
640
+
641
+ return (
642
+ <div className="group rounded-xl border border-border bg-card p-3.5 hover:border-[var(--amber)]/30 transition-all duration-150">
643
+ <div className="flex items-center gap-2.5">
644
+ <div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
645
+ <Globe size={14} className="text-muted-foreground" />
646
+ </div>
647
+ <div className="flex-1 min-w-0">
648
+ <p className="text-sm font-medium text-foreground truncate">{agent.card.name}</p>
649
+ <p className="text-2xs text-muted-foreground truncate">{agent.card.description}</p>
650
+ </div>
651
+ <StatusIcon size={13} className={statusColor} aria-hidden="true" />
652
+ <button
653
+ type="button"
654
+ onClick={() => onRemove(agent.id)}
655
+ 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"
656
+ aria-label={removeCopy}
657
+ title={removeCopy}
658
+ >
659
+ <Trash2 size={12} />
660
+ </button>
661
+ </div>
662
+ {agent.card.skills.length > 0 && (
663
+ <div className="mt-2.5 pt-2 border-t border-border/40 flex items-center gap-1.5">
664
+ <Zap size={11} className="text-muted-foreground/60 shrink-0" aria-hidden="true" />
665
+ <span className="text-2xs text-muted-foreground">{skillsCopy}: {agent.card.skills.length}</span>
666
+ <div className="flex flex-wrap gap-1 ml-1">
667
+ {agent.card.skills.slice(0, 3).map((s) => (
668
+ <span
669
+ key={s.id}
670
+ className="text-2xs px-1.5 py-0.5 rounded bg-muted/80 text-muted-foreground border border-border/50"
671
+ title={s.description}
672
+ >
673
+ {s.name}
674
+ </span>
675
+ ))}
676
+ {agent.card.skills.length > 3 && (
677
+ <span className="text-2xs text-muted-foreground/60">+{agent.card.skills.length - 3}</span>
678
+ )}
679
+ </div>
680
+ </div>
681
+ )}
682
+ </div>
683
+ );
684
+ }