@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.
- 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 +51 -6
- package/app/components/agents/AgentsContentPage.tsx +24 -6
- package/app/components/agents/AgentsOverviewSection.tsx +11 -0
- 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/ask/AskContent.tsx +8 -0
- package/app/components/help/HelpContent.tsx +74 -18
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelAgentListRow.tsx +10 -1
- 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
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Search, X, FileText, Table } from 'lucide-react';
|
|
5
|
+
import { Search, X, FileText, Table, Settings, RotateCcw, Moon, Sun, Bot, Compass, HelpCircle } from 'lucide-react';
|
|
6
6
|
import { SearchResult } from '@/lib/types';
|
|
7
7
|
import { encodePath } from '@/lib/utils';
|
|
8
8
|
import { apiFetch } from '@/lib/api';
|
|
9
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
+
import { toast } from '@/lib/toast';
|
|
10
11
|
|
|
11
12
|
interface SearchModalProps {
|
|
12
13
|
open: boolean;
|
|
13
14
|
onClose: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
type PaletteTab = 'search' | 'actions';
|
|
18
|
+
|
|
19
|
+
interface CommandAction {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon: React.ReactNode;
|
|
23
|
+
shortcut?: string;
|
|
24
|
+
execute: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/** Highlight matched text fragments in a snippet based on the query */
|
|
17
28
|
function highlightSnippet(snippet: string, query: string): React.ReactNode {
|
|
18
29
|
if (!query.trim()) return snippet;
|
|
19
|
-
// Split query into words and escape for regex
|
|
20
30
|
const words = query.trim().split(/\s+/).filter(Boolean);
|
|
21
31
|
const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
22
32
|
const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
|
|
@@ -31,12 +41,74 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
31
41
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
32
42
|
const [loading, setLoading] = useState(false);
|
|
33
43
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
44
|
+
const [tab, setTab] = useState<PaletteTab>('search');
|
|
45
|
+
const [actionIndex, setActionIndex] = useState(0);
|
|
34
46
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
35
47
|
const resultsRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
const actionsRef = useRef<HTMLDivElement>(null);
|
|
36
49
|
const router = useRouter();
|
|
37
50
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
38
51
|
const { t } = useLocale();
|
|
39
52
|
|
|
53
|
+
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
54
|
+
|
|
55
|
+
const actions: CommandAction[] = useMemo(() => [
|
|
56
|
+
{
|
|
57
|
+
id: 'settings',
|
|
58
|
+
label: t.search.openSettings,
|
|
59
|
+
icon: <Settings size={15} />,
|
|
60
|
+
shortcut: '⌘,',
|
|
61
|
+
execute: () => { router.push('/settings'); onClose(); },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'restart-walkthrough',
|
|
65
|
+
label: t.search.restartWalkthrough,
|
|
66
|
+
icon: <RotateCcw size={15} />,
|
|
67
|
+
execute: () => {
|
|
68
|
+
fetch('/api/setup', {
|
|
69
|
+
method: 'PATCH',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({ walkthroughStep: 0, walkthroughDismissed: false }),
|
|
72
|
+
}).then(() => {
|
|
73
|
+
toast.success(t.search.walkthroughRestarted);
|
|
74
|
+
}).catch(() => {
|
|
75
|
+
toast.error('Failed to restart walkthrough');
|
|
76
|
+
});
|
|
77
|
+
onClose();
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'toggle-dark-mode',
|
|
82
|
+
label: t.search.toggleDarkMode,
|
|
83
|
+
icon: isDark ? <Sun size={15} /> : <Moon size={15} />,
|
|
84
|
+
execute: () => {
|
|
85
|
+
const html = document.documentElement;
|
|
86
|
+
const nowDark = html.classList.contains('dark');
|
|
87
|
+
html.classList.toggle('dark', !nowDark);
|
|
88
|
+
try { localStorage.setItem('theme', nowDark ? 'light' : 'dark'); } catch { /* noop */ }
|
|
89
|
+
onClose();
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'go-agents',
|
|
94
|
+
label: t.search.goToAgents,
|
|
95
|
+
icon: <Bot size={15} />,
|
|
96
|
+
execute: () => { router.push('/agents'); onClose(); },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'go-discover',
|
|
100
|
+
label: t.search.goToDiscover,
|
|
101
|
+
icon: <Compass size={15} />,
|
|
102
|
+
execute: () => { router.push('/discover'); onClose(); },
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'go-help',
|
|
106
|
+
label: t.search.goToHelp,
|
|
107
|
+
icon: <HelpCircle size={15} />,
|
|
108
|
+
execute: () => { router.push('/help'); onClose(); },
|
|
109
|
+
},
|
|
110
|
+
], [t, router, onClose, isDark]);
|
|
111
|
+
|
|
40
112
|
// Focus input when modal opens
|
|
41
113
|
useEffect(() => {
|
|
42
114
|
if (open) {
|
|
@@ -44,6 +116,8 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
44
116
|
setQuery('');
|
|
45
117
|
setResults([]);
|
|
46
118
|
setSelectedIndex(0);
|
|
119
|
+
setTab('search');
|
|
120
|
+
setActionIndex(0);
|
|
47
121
|
}
|
|
48
122
|
}, [open]);
|
|
49
123
|
|
|
@@ -86,26 +160,51 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
86
160
|
const handler = (e: KeyboardEvent) => {
|
|
87
161
|
if (e.key === 'Escape') {
|
|
88
162
|
onClose();
|
|
163
|
+
} else if (e.key === 'Tab') {
|
|
164
|
+
// Tab switches between Search/Actions tabs
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
setTab(prev => prev === 'search' ? 'actions' : 'search');
|
|
167
|
+
setActionIndex(0);
|
|
168
|
+
setSelectedIndex(0);
|
|
89
169
|
} else if (e.key === 'ArrowDown') {
|
|
90
170
|
e.preventDefault();
|
|
91
|
-
|
|
171
|
+
if (tab === 'search') {
|
|
172
|
+
setSelectedIndex(i => Math.min(i + 1, results.length - 1));
|
|
173
|
+
} else {
|
|
174
|
+
setActionIndex(i => Math.min(i + 1, actions.length - 1));
|
|
175
|
+
}
|
|
92
176
|
} else if (e.key === 'ArrowUp') {
|
|
93
177
|
e.preventDefault();
|
|
94
|
-
|
|
178
|
+
if (tab === 'search') {
|
|
179
|
+
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
180
|
+
} else {
|
|
181
|
+
setActionIndex(i => Math.max(i - 1, 0));
|
|
182
|
+
}
|
|
95
183
|
} else if (e.key === 'Enter') {
|
|
96
|
-
if (
|
|
184
|
+
if (tab === 'search') {
|
|
185
|
+
if (results[selectedIndex]) navigate(results[selectedIndex]);
|
|
186
|
+
} else {
|
|
187
|
+
if (actions[actionIndex]) actions[actionIndex].execute();
|
|
188
|
+
}
|
|
97
189
|
}
|
|
98
190
|
};
|
|
99
191
|
window.addEventListener('keydown', handler);
|
|
100
192
|
return () => window.removeEventListener('keydown', handler);
|
|
101
|
-
}, [open, onClose, results, selectedIndex, navigate]);
|
|
193
|
+
}, [open, onClose, results, selectedIndex, navigate, tab, actions, actionIndex]);
|
|
102
194
|
|
|
103
195
|
useLayoutEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
196
|
+
if (tab === 'search') {
|
|
197
|
+
const container = resultsRef.current;
|
|
198
|
+
if (!container) return;
|
|
199
|
+
const selected = container.children[selectedIndex] as HTMLElement | undefined;
|
|
200
|
+
selected?.scrollIntoView({ block: 'nearest' });
|
|
201
|
+
} else {
|
|
202
|
+
const container = actionsRef.current;
|
|
203
|
+
if (!container) return;
|
|
204
|
+
const selected = container.children[actionIndex] as HTMLElement | undefined;
|
|
205
|
+
selected?.scrollIntoView({ block: 'nearest' });
|
|
206
|
+
}
|
|
207
|
+
}, [selectedIndex, actionIndex, tab]);
|
|
109
208
|
|
|
110
209
|
if (!open) return null;
|
|
111
210
|
|
|
@@ -114,85 +213,140 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
114
213
|
className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[15vh] modal-backdrop"
|
|
115
214
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
116
215
|
>
|
|
117
|
-
<div role="dialog" aria-modal="true" aria-label="
|
|
216
|
+
<div role="dialog" aria-modal="true" aria-label="Command palette" className="w-full md:max-w-xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl overflow-hidden max-h-[85vh] md:max-h-none flex flex-col">
|
|
118
217
|
{/* Mobile drag indicator */}
|
|
119
218
|
<div className="flex justify-center pt-2 pb-0 md:hidden">
|
|
120
219
|
<div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
|
|
121
220
|
</div>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
221
|
+
|
|
222
|
+
{/* Tab bar */}
|
|
223
|
+
<div className="flex items-center gap-1 px-4 pt-2 pb-0">
|
|
224
|
+
<button
|
|
225
|
+
onClick={() => { setTab('search'); setTimeout(() => inputRef.current?.focus(), 50); }}
|
|
226
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-t transition-colors ${
|
|
227
|
+
tab === 'search'
|
|
228
|
+
? 'text-foreground border-b-2 border-[var(--amber)]'
|
|
229
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
230
|
+
}`}
|
|
231
|
+
>
|
|
232
|
+
{t.search.tabSearch}
|
|
233
|
+
</button>
|
|
234
|
+
<button
|
|
235
|
+
onClick={() => { setTab('actions'); setActionIndex(0); }}
|
|
236
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-t transition-colors ${
|
|
237
|
+
tab === 'actions'
|
|
238
|
+
? 'text-foreground border-b-2 border-[var(--amber)]'
|
|
239
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
{t.search.tabActions}
|
|
243
|
+
</button>
|
|
142
244
|
</div>
|
|
143
245
|
|
|
144
|
-
{/*
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
246
|
+
{/* Search tab */}
|
|
247
|
+
{tab === 'search' && (
|
|
248
|
+
<>
|
|
249
|
+
{/* Search input */}
|
|
250
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-border">
|
|
251
|
+
<Search size={16} className="text-muted-foreground shrink-0" />
|
|
252
|
+
<input
|
|
253
|
+
ref={inputRef}
|
|
254
|
+
type="text"
|
|
255
|
+
value={query}
|
|
256
|
+
onChange={handleChange}
|
|
257
|
+
placeholder={t.search.placeholder}
|
|
258
|
+
className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none"
|
|
259
|
+
/>
|
|
260
|
+
{loading && (
|
|
261
|
+
<div className="w-4 h-4 border-2 border-muted-foreground/40 border-t-foreground rounded-full animate-spin shrink-0" />
|
|
262
|
+
)}
|
|
263
|
+
{!loading && query && (
|
|
264
|
+
<button onClick={() => { setQuery(''); setResults([]); inputRef.current?.focus(); }}>
|
|
265
|
+
<X size={14} className="text-muted-foreground hover:text-foreground" />
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
<kbd className="hidden md:inline text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5 font-mono">ESC</kbd>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{/* Results */}
|
|
272
|
+
<div ref={resultsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
|
|
273
|
+
{results.length === 0 && query && !loading && (
|
|
274
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
|
|
275
|
+
)}
|
|
276
|
+
{results.length === 0 && !query && (
|
|
277
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground/60">{t.search.prompt}</div>
|
|
278
|
+
)}
|
|
279
|
+
{results.map((result, i) => {
|
|
280
|
+
const ext = result.path.endsWith('.csv') ? '.csv' : '.md';
|
|
281
|
+
const parts = result.path.split('/');
|
|
282
|
+
const fileName = parts[parts.length - 1];
|
|
283
|
+
const dirPath = parts.slice(0, -1).join('/');
|
|
284
|
+
return (
|
|
285
|
+
<button
|
|
286
|
+
key={result.path}
|
|
287
|
+
onClick={() => navigate(result)}
|
|
288
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
289
|
+
className={`
|
|
290
|
+
w-full px-4 py-3 flex items-start gap-3 text-left transition-colors duration-75
|
|
291
|
+
${i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'}
|
|
292
|
+
${i < results.length - 1 ? 'border-b border-border' : ''}
|
|
293
|
+
`}
|
|
294
|
+
>
|
|
295
|
+
{ext === '.csv'
|
|
296
|
+
? <Table size={14} className="text-success shrink-0 mt-0.5" />
|
|
297
|
+
: <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
|
|
298
|
+
}
|
|
299
|
+
<div className="min-w-0 flex-1">
|
|
300
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
301
|
+
<span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
|
|
302
|
+
{dirPath && (
|
|
303
|
+
<span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
{result.snippet && (
|
|
307
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
|
|
308
|
+
{highlightSnippet(result.snippet, query)}
|
|
309
|
+
</p>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</button>
|
|
313
|
+
);
|
|
314
|
+
})}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Footer — desktop only */}
|
|
318
|
+
{results.length > 0 && (
|
|
319
|
+
<div className="hidden md:flex px-4 py-2 border-t border-border items-center gap-3 text-xs text-muted-foreground/60">
|
|
320
|
+
<span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
|
|
321
|
+
<span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
|
|
322
|
+
<span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{/* Actions tab */}
|
|
329
|
+
{tab === 'actions' && (
|
|
330
|
+
<div ref={actionsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1 py-1">
|
|
331
|
+
{actions.map((action, i) => (
|
|
158
332
|
<button
|
|
159
|
-
key={
|
|
160
|
-
onClick={() =>
|
|
161
|
-
onMouseEnter={() =>
|
|
333
|
+
key={action.id}
|
|
334
|
+
onClick={() => action.execute()}
|
|
335
|
+
onMouseEnter={() => setActionIndex(i)}
|
|
162
336
|
className={`
|
|
163
|
-
w-full px-4 py-
|
|
164
|
-
${i ===
|
|
165
|
-
${i < results.length - 1 ? 'border-b border-border' : ''}
|
|
337
|
+
w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors duration-75
|
|
338
|
+
${i === actionIndex ? 'bg-muted' : 'hover:bg-muted/50'}
|
|
166
339
|
`}
|
|
167
340
|
>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{dirPath && (
|
|
176
|
-
<span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
|
|
177
|
-
)}
|
|
178
|
-
</div>
|
|
179
|
-
{result.snippet && (
|
|
180
|
-
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
|
|
181
|
-
{highlightSnippet(result.snippet, query)}
|
|
182
|
-
</p>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
341
|
+
<span className="text-muted-foreground shrink-0">{action.icon}</span>
|
|
342
|
+
<span className="text-sm text-foreground flex-1">{action.label}</span>
|
|
343
|
+
{action.shortcut && (
|
|
344
|
+
<kbd className="text-xs text-muted-foreground/60 font-mono border border-border rounded px-1.5 py-0.5">
|
|
345
|
+
{action.shortcut}
|
|
346
|
+
</kbd>
|
|
347
|
+
)}
|
|
185
348
|
</button>
|
|
186
|
-
)
|
|
187
|
-
})}
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
{/* Footer — desktop only */}
|
|
191
|
-
{results.length > 0 && (
|
|
192
|
-
<div className="hidden md:flex px-4 py-2 border-t border-border items-center gap-3 text-xs text-muted-foreground/60">
|
|
193
|
-
<span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
|
|
194
|
-
<span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
|
|
195
|
-
<span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
|
|
349
|
+
))}
|
|
196
350
|
</div>
|
|
197
351
|
)}
|
|
198
352
|
</div>
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
-
import { ArrowLeft, Server, Search, Trash2, Zap } from 'lucide-react';
|
|
5
|
+
import { ArrowLeft, ChevronDown, Globe, Server, Search, Trash2, Zap } from 'lucide-react';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
6
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
import { toast } from '@/lib/toast';
|
|
7
9
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
10
|
+
import { useA2aRegistry } from '@/hooks/useA2aRegistry';
|
|
8
11
|
import { apiFetch } from '@/lib/api';
|
|
9
12
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
10
13
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
@@ -21,7 +24,9 @@ import SkillDetailPopover from './SkillDetailPopover';
|
|
|
21
24
|
export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
22
25
|
const { t } = useLocale();
|
|
23
26
|
const a = t.agentsContent;
|
|
27
|
+
const p = t.panels.agents;
|
|
24
28
|
const mcp = useMcpData();
|
|
29
|
+
const a2a = useA2aRegistry();
|
|
25
30
|
|
|
26
31
|
const agent = useMemo(() => mcp.agents.find((item) => item.key === agentKey), [mcp.agents, agentKey]);
|
|
27
32
|
const [skillQuery, setSkillQuery] = useState('');
|
|
@@ -31,7 +36,6 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
31
36
|
const [editContent, setEditContent] = useState('');
|
|
32
37
|
const [editError, setEditError] = useState<string | null>(null);
|
|
33
38
|
const [saveBusy, setSaveBusy] = useState(false);
|
|
34
|
-
const [snippetCopied, setSnippetCopied] = useState(false);
|
|
35
39
|
const [mcpBusy, setMcpBusy] = useState(false);
|
|
36
40
|
const [mcpMessage, setMcpMessage] = useState<string | null>(null);
|
|
37
41
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
@@ -39,6 +43,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
39
43
|
const [confirmMcpRemove, setConfirmMcpRemove] = useState<string | null>(null);
|
|
40
44
|
const [mcpHint, setMcpHint] = useState<string | null>(null);
|
|
41
45
|
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
|
46
|
+
const [a2aOpen, setA2aOpen] = useState(false);
|
|
42
47
|
|
|
43
48
|
const filteredSkills = useMemo(
|
|
44
49
|
() =>
|
|
@@ -157,9 +162,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
157
162
|
|
|
158
163
|
const handleCopySnippet = useCallback(async () => {
|
|
159
164
|
const ok = await copyToClipboard(snippet.snippet);
|
|
160
|
-
if (
|
|
161
|
-
setSnippetCopied(true);
|
|
162
|
-
setTimeout(() => setSnippetCopied(false), 1200);
|
|
165
|
+
if (ok) toast.copy();
|
|
163
166
|
}, [snippet.snippet]);
|
|
164
167
|
|
|
165
168
|
const handleApplyMcpConfig = useCallback(async (scope: 'project' | 'global', transport: 'stdio' | 'http') => {
|
|
@@ -272,7 +275,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
272
275
|
onClick={() => void handleCopySnippet()}
|
|
273
276
|
disabled={false}
|
|
274
277
|
busy={false}
|
|
275
|
-
label={
|
|
278
|
+
label={a.detail.mcpCopySnippet}
|
|
276
279
|
/>
|
|
277
280
|
)}
|
|
278
281
|
<ActionButton
|
|
@@ -358,6 +361,48 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
358
361
|
</div>
|
|
359
362
|
</section>
|
|
360
363
|
|
|
364
|
+
{/* ═══════════ A2A CAPABILITIES ═══════════ */}
|
|
365
|
+
<section className="rounded-xl border border-border bg-card overflow-hidden">
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={() => setA2aOpen(!a2aOpen)}
|
|
369
|
+
className="w-full flex items-center justify-between gap-2 px-4 py-3 text-left hover:bg-muted/20 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
370
|
+
aria-expanded={a2aOpen}
|
|
371
|
+
>
|
|
372
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
373
|
+
<Globe size={12} className="text-muted-foreground/50" />
|
|
374
|
+
{p.a2aCapabilities}
|
|
375
|
+
</h2>
|
|
376
|
+
<ChevronDown
|
|
377
|
+
size={13}
|
|
378
|
+
className={cn('shrink-0 text-muted-foreground/50 transition-transform duration-200', a2aOpen && 'rotate-180')}
|
|
379
|
+
aria-hidden="true"
|
|
380
|
+
/>
|
|
381
|
+
</button>
|
|
382
|
+
<div
|
|
383
|
+
className={cn(
|
|
384
|
+
'grid transition-[grid-template-rows] duration-250 ease-out',
|
|
385
|
+
a2aOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
388
|
+
<div className="overflow-hidden" {...(!a2aOpen && { inert: true } as React.HTMLAttributes<HTMLDivElement>)}>
|
|
389
|
+
<div className="px-4 pb-3 pt-1 space-y-2 border-t border-border/40">
|
|
390
|
+
<div className="flex items-baseline gap-2 px-0.5 min-w-0">
|
|
391
|
+
<span className="text-2xs text-muted-foreground/50 uppercase tracking-wider shrink-0 min-w-[60px]">{p.a2aStatus}</span>
|
|
392
|
+
<span className={`text-xs font-medium ${
|
|
393
|
+
status === 'connected' ? 'text-[var(--success)]' : 'text-muted-foreground'
|
|
394
|
+
}`}>
|
|
395
|
+
{status === 'connected' ? p.a2aConnected : p.a2aUnavailable}
|
|
396
|
+
</span>
|
|
397
|
+
</div>
|
|
398
|
+
{a2a.agents.length === 0 && (
|
|
399
|
+
<p className="text-2xs text-muted-foreground/50">{p.a2aNoRemoteHint}</p>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</section>
|
|
405
|
+
|
|
361
406
|
{/* ═══════════ SKILL ASSIGNMENTS ═══════════ */}
|
|
362
407
|
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
363
408
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { toast } from '@/lib/toast';
|
|
4
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
5
6
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
7
|
+
import { useA2aRegistry } from '@/hooks/useA2aRegistry';
|
|
6
8
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
9
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
8
10
|
import {
|
|
@@ -13,13 +15,20 @@ import {
|
|
|
13
15
|
import AgentsOverviewSection from './AgentsOverviewSection';
|
|
14
16
|
import AgentsMcpSection from './AgentsMcpSection';
|
|
15
17
|
import AgentsSkillsSection from './AgentsSkillsSection';
|
|
18
|
+
import AgentsPanelA2aTab from './AgentsPanelA2aTab';
|
|
16
19
|
|
|
17
20
|
export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab }) {
|
|
18
21
|
const { t } = useLocale();
|
|
19
22
|
const a = t.agentsContent;
|
|
20
23
|
const mcp = useMcpData();
|
|
21
|
-
const
|
|
24
|
+
const a2a = useA2aRegistry();
|
|
22
25
|
const pageHeader = useMemo(() => {
|
|
26
|
+
if (tab === 'a2a') {
|
|
27
|
+
return {
|
|
28
|
+
title: a.navNetwork,
|
|
29
|
+
subtitle: a.a2aTabEmptyHint,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
23
32
|
if (tab === 'skills') {
|
|
24
33
|
return {
|
|
25
34
|
title: a.navSkills,
|
|
@@ -60,9 +69,7 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
60
69
|
if (!agent) return;
|
|
61
70
|
const snippet = generateSnippet(agent, mcp.status, agent.preferredTransport);
|
|
62
71
|
const ok = await copyToClipboard(snippet.snippet);
|
|
63
|
-
if (
|
|
64
|
-
setCopyState(agentKey);
|
|
65
|
-
setTimeout(() => setCopyState(null), 1500);
|
|
72
|
+
if (ok) toast.copy();
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
return (
|
|
@@ -86,16 +93,27 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
86
93
|
enabledSkillCount={enabledSkillCount}
|
|
87
94
|
allAgents={mcp.agents}
|
|
88
95
|
pulseCopy={a.workspacePulse}
|
|
96
|
+
a2aCount={a2a.agents.length}
|
|
89
97
|
/>
|
|
90
98
|
)}
|
|
91
99
|
|
|
92
100
|
{tab === 'mcp' && (
|
|
93
|
-
<AgentsMcpSection copy={{ ...a.mcp, status: a.status }} mcp={mcp} buckets={buckets} copyState={
|
|
101
|
+
<AgentsMcpSection copy={{ ...a.mcp, status: a.status }} mcp={mcp} buckets={buckets} copyState={null} onCopySnippet={copySnippet} />
|
|
94
102
|
)}
|
|
95
103
|
|
|
96
104
|
{tab === 'skills' && (
|
|
97
105
|
<AgentsSkillsSection copy={a.skills} mcp={mcp} buckets={buckets} />
|
|
98
106
|
)}
|
|
107
|
+
|
|
108
|
+
{tab === 'a2a' && (
|
|
109
|
+
<AgentsPanelA2aTab
|
|
110
|
+
agents={a2a.agents}
|
|
111
|
+
discovering={a2a.discovering}
|
|
112
|
+
error={a2a.error}
|
|
113
|
+
onDiscover={a2a.discover}
|
|
114
|
+
onRemove={a2a.remove}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
99
117
|
</div>
|
|
100
118
|
);
|
|
101
119
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ArrowRight,
|
|
8
8
|
Cable,
|
|
9
9
|
ChevronDown,
|
|
10
|
+
Globe,
|
|
10
11
|
Server,
|
|
11
12
|
Zap,
|
|
12
13
|
} from 'lucide-react';
|
|
@@ -63,6 +64,7 @@ export default function AgentsOverviewSection({
|
|
|
63
64
|
enabledSkillCount,
|
|
64
65
|
allAgents,
|
|
65
66
|
pulseCopy,
|
|
67
|
+
a2aCount,
|
|
66
68
|
}: {
|
|
67
69
|
copy: OverviewCopy;
|
|
68
70
|
buckets: AgentBuckets;
|
|
@@ -73,6 +75,7 @@ export default function AgentsOverviewSection({
|
|
|
73
75
|
enabledSkillCount: number;
|
|
74
76
|
allAgents: AgentInfo[];
|
|
75
77
|
pulseCopy: PulseCopy;
|
|
78
|
+
a2aCount?: number;
|
|
76
79
|
}) {
|
|
77
80
|
const allHealthy = riskQueue.length === 0 && mcpRunning;
|
|
78
81
|
const totalAgents = allAgents.length;
|
|
@@ -128,6 +131,14 @@ export default function AgentsOverviewSection({
|
|
|
128
131
|
value={mcpRunning ? `:${mcpPort}` : '—'}
|
|
129
132
|
tone={mcpRunning ? 'ok' : 'warn'}
|
|
130
133
|
/>
|
|
134
|
+
{a2aCount != null && (
|
|
135
|
+
<StatCell
|
|
136
|
+
icon={<Globe size={14} aria-hidden="true" />}
|
|
137
|
+
label={copy.a2aLabel as string ?? 'A2A'}
|
|
138
|
+
value={a2aCount}
|
|
139
|
+
tone={a2aCount > 0 ? 'ok' : 'muted'}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
131
142
|
</div>
|
|
132
143
|
</section>
|
|
133
144
|
|