@geminilight/mindos 0.6.29 → 0.6.31
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/README.md +10 -4
- package/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useTransition } from 'react';
|
|
4
|
+
import { Trash2, RotateCcw, X, Folder, FileText, Table, AlertTriangle, Loader2 } from 'lucide-react';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { restoreFromTrashAction, permanentlyDeleteAction, emptyTrashAction } from '@/lib/actions';
|
|
8
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
9
|
+
import { toast } from '@/lib/toast';
|
|
10
|
+
import type { TrashMeta } from '@/lib/core/trash';
|
|
11
|
+
|
|
12
|
+
function relativeTimeShort(iso: string): string {
|
|
13
|
+
const delta = Date.now() - new Date(iso).getTime();
|
|
14
|
+
const mins = Math.floor(delta / 60000);
|
|
15
|
+
if (mins < 1) return 'just now';
|
|
16
|
+
if (mins < 60) return `${mins}m ago`;
|
|
17
|
+
const hours = Math.floor(mins / 60);
|
|
18
|
+
if (hours < 24) return `${hours}h ago`;
|
|
19
|
+
const days = Math.floor(hours / 24);
|
|
20
|
+
return `${days}d ago`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function daysUntil(iso: string): number {
|
|
24
|
+
return Math.max(0, Math.ceil((new Date(iso).getTime() - Date.now()) / 86400000));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function TrashPageClient({ initialItems }: { initialItems: TrashMeta[] }) {
|
|
28
|
+
const { t } = useLocale();
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const [items, setItems] = useState(initialItems);
|
|
31
|
+
const [isPending, startTransition] = useTransition();
|
|
32
|
+
const [confirmEmpty, setConfirmEmpty] = useState(false);
|
|
33
|
+
const [confirmDelete, setConfirmDelete] = useState<TrashMeta | null>(null);
|
|
34
|
+
const [conflictItem, setConflictItem] = useState<TrashMeta | null>(null);
|
|
35
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const handleRestore = useCallback(async (item: TrashMeta) => {
|
|
38
|
+
setBusyId(item.id);
|
|
39
|
+
try {
|
|
40
|
+
const result = await restoreFromTrashAction(item.id, 'restore');
|
|
41
|
+
if (result.success) {
|
|
42
|
+
setItems(prev => prev.filter(i => i.id !== item.id));
|
|
43
|
+
toast.success(t.trash.restored);
|
|
44
|
+
router.refresh();
|
|
45
|
+
} else if (result.conflict) {
|
|
46
|
+
setConflictItem(item);
|
|
47
|
+
} else {
|
|
48
|
+
toast.error(result.error ?? 'Failed to restore');
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
setBusyId(null);
|
|
52
|
+
}
|
|
53
|
+
}, [t, router]);
|
|
54
|
+
|
|
55
|
+
const handleConflictResolve = useCallback(async (mode: 'overwrite' | 'copy') => {
|
|
56
|
+
if (!conflictItem) return;
|
|
57
|
+
const result = await restoreFromTrashAction(conflictItem.id, mode);
|
|
58
|
+
if (result.success) {
|
|
59
|
+
setItems(prev => prev.filter(i => i.id !== conflictItem.id));
|
|
60
|
+
toast.success(t.trash.restored);
|
|
61
|
+
setConflictItem(null);
|
|
62
|
+
router.refresh();
|
|
63
|
+
} else {
|
|
64
|
+
toast.error(result.error ?? 'Failed to restore');
|
|
65
|
+
}
|
|
66
|
+
}, [conflictItem, t, router]);
|
|
67
|
+
|
|
68
|
+
const handlePermanentDelete = useCallback(async () => {
|
|
69
|
+
if (!confirmDelete) return;
|
|
70
|
+
const result = await permanentlyDeleteAction(confirmDelete.id);
|
|
71
|
+
if (result.success) {
|
|
72
|
+
setItems(prev => prev.filter(i => i.id !== confirmDelete.id));
|
|
73
|
+
toast.success(t.trash.deleted);
|
|
74
|
+
} else {
|
|
75
|
+
toast.error(result.error ?? 'Failed to delete');
|
|
76
|
+
}
|
|
77
|
+
setConfirmDelete(null);
|
|
78
|
+
}, [confirmDelete, t]);
|
|
79
|
+
|
|
80
|
+
const handleEmptyTrash = useCallback(async () => {
|
|
81
|
+
startTransition(async () => {
|
|
82
|
+
const result = await emptyTrashAction();
|
|
83
|
+
if (result.success) {
|
|
84
|
+
setItems([]);
|
|
85
|
+
toast.success(t.trash.emptied(result.count ?? 0));
|
|
86
|
+
} else {
|
|
87
|
+
toast.error(result.error ?? 'Failed to empty trash');
|
|
88
|
+
}
|
|
89
|
+
setConfirmEmpty(false);
|
|
90
|
+
});
|
|
91
|
+
}, [t]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="min-h-screen">
|
|
95
|
+
<div className="px-4 md:px-6 pt-6 md:pt-8">
|
|
96
|
+
<div className="content-width xl:mr-[220px] rounded-xl border border-border bg-card px-4 py-3 md:px-5 md:py-4">
|
|
97
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
98
|
+
<div className="min-w-0">
|
|
99
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground font-display">
|
|
100
|
+
<Trash2 size={15} />
|
|
101
|
+
{t.trash.title}
|
|
102
|
+
</div>
|
|
103
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
104
|
+
{t.trash.subtitle}
|
|
105
|
+
</p>
|
|
106
|
+
{items.length > 0 && (
|
|
107
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
108
|
+
{t.trash.itemCount(items.length)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
{items.length > 0 && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={() => setConfirmEmpty(true)}
|
|
116
|
+
disabled={isPending}
|
|
117
|
+
className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-error/10 text-error hover:bg-error/20 transition-colors disabled:opacity-50"
|
|
118
|
+
>
|
|
119
|
+
{t.trash.emptyTrash}
|
|
120
|
+
</button>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="px-4 md:px-6 py-4 md:py-6">
|
|
127
|
+
<div className="content-width xl:mr-[220px]">
|
|
128
|
+
{items.length === 0 ? (
|
|
129
|
+
<div className="rounded-xl border border-dashed border-border bg-card p-12 text-center">
|
|
130
|
+
<Trash2 size={32} className="mx-auto text-muted-foreground/30 mb-3" />
|
|
131
|
+
<p className="text-sm text-muted-foreground font-display">{t.trash.empty}</p>
|
|
132
|
+
<p className="text-xs text-muted-foreground/60 mt-1">{t.trash.emptySubtext}</p>
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
136
|
+
{items.map(item => {
|
|
137
|
+
const days = daysUntil(item.expiresAt);
|
|
138
|
+
const isExpiring = days <= 3;
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={item.id}
|
|
142
|
+
className="rounded-xl border border-border bg-card p-4 flex flex-col gap-3 hover:border-border/80 transition-colors"
|
|
143
|
+
>
|
|
144
|
+
<div className="flex items-start gap-3 min-w-0">
|
|
145
|
+
<div className="w-9 h-9 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
|
146
|
+
{item.isDirectory
|
|
147
|
+
? <Folder size={16} className="text-muted-foreground" />
|
|
148
|
+
: item.fileName.endsWith('.csv')
|
|
149
|
+
? <Table size={16} className="text-success" />
|
|
150
|
+
: <FileText size={16} className="text-muted-foreground" />
|
|
151
|
+
}
|
|
152
|
+
</div>
|
|
153
|
+
<div className="min-w-0 flex-1">
|
|
154
|
+
<p className="text-sm font-medium text-foreground truncate" title={item.fileName}>
|
|
155
|
+
{item.fileName}
|
|
156
|
+
</p>
|
|
157
|
+
<p className="text-xs text-muted-foreground truncate mt-0.5" title={item.originalPath}>
|
|
158
|
+
{t.trash.from}: {item.originalPath.split('/').slice(0, -1).join('/') || '/'}
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex items-center justify-between">
|
|
163
|
+
<div className="flex flex-col gap-0.5">
|
|
164
|
+
<span className="text-2xs text-muted-foreground">
|
|
165
|
+
{t.trash.deletedAgo(relativeTimeShort(item.deletedAt))}
|
|
166
|
+
</span>
|
|
167
|
+
<span className={`text-2xs ${isExpiring ? 'text-error' : 'text-muted-foreground/60'}`}>
|
|
168
|
+
{isExpiring && <AlertTriangle size={9} className="inline mr-0.5" />}
|
|
169
|
+
{t.trash.expiresIn(days)}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="flex items-center gap-1.5">
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
onClick={() => handleRestore(item)}
|
|
176
|
+
disabled={busyId === item.id}
|
|
177
|
+
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium bg-success/10 text-success hover:bg-success/20 transition-colors disabled:opacity-50"
|
|
178
|
+
>
|
|
179
|
+
{busyId === item.id ? <Loader2 size={11} className="animate-spin" /> : <RotateCcw size={11} />}
|
|
180
|
+
{t.trash.restore}
|
|
181
|
+
</button>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => setConfirmDelete(item)}
|
|
185
|
+
className="p-1 rounded-md text-muted-foreground hover:text-error hover:bg-error/10 transition-colors"
|
|
186
|
+
title={t.trash.deletePermanently}
|
|
187
|
+
>
|
|
188
|
+
<X size={13} />
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Empty Trash Confirmation */}
|
|
201
|
+
<ConfirmDialog
|
|
202
|
+
open={confirmEmpty}
|
|
203
|
+
title={t.trash.emptyTrash}
|
|
204
|
+
message={t.trash.emptyTrashConfirm}
|
|
205
|
+
confirmLabel={t.trash.emptyTrash}
|
|
206
|
+
cancelLabel={t.export?.cancel ?? 'Cancel'}
|
|
207
|
+
onConfirm={() => void handleEmptyTrash()}
|
|
208
|
+
onCancel={() => setConfirmEmpty(false)}
|
|
209
|
+
variant="destructive"
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
{/* Permanent Delete Confirmation */}
|
|
213
|
+
<ConfirmDialog
|
|
214
|
+
open={!!confirmDelete}
|
|
215
|
+
title={t.trash.deletePermanently}
|
|
216
|
+
message={confirmDelete ? t.trash.deletePermanentlyConfirm(confirmDelete.fileName) : ''}
|
|
217
|
+
confirmLabel={t.trash.deletePermanently}
|
|
218
|
+
cancelLabel={t.export?.cancel ?? 'Cancel'}
|
|
219
|
+
onConfirm={() => void handlePermanentDelete()}
|
|
220
|
+
onCancel={() => setConfirmDelete(null)}
|
|
221
|
+
variant="destructive"
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
{/* Restore Conflict Dialog */}
|
|
225
|
+
{conflictItem && (
|
|
226
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
|
227
|
+
<div className="bg-card border border-border rounded-xl shadow-xl max-w-sm w-full mx-4 p-5">
|
|
228
|
+
<div className="flex items-center gap-2 mb-3">
|
|
229
|
+
<AlertTriangle size={16} className="text-[var(--amber)]" />
|
|
230
|
+
<h3 className="text-sm font-semibold font-display">{t.trash.restoreConflict}</h3>
|
|
231
|
+
</div>
|
|
232
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
233
|
+
"{conflictItem.fileName}" — {conflictItem.originalPath}
|
|
234
|
+
</p>
|
|
235
|
+
<div className="flex items-center gap-2 justify-end">
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
onClick={() => setConflictItem(null)}
|
|
239
|
+
className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
240
|
+
>
|
|
241
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => void handleConflictResolve('copy')}
|
|
246
|
+
className="px-3 py-1.5 rounded-md text-xs font-medium bg-muted text-foreground hover:bg-muted/80 transition-colors"
|
|
247
|
+
>
|
|
248
|
+
{t.trash.saveAsCopy}
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
onClick={() => void handleConflictResolve('overwrite')}
|
|
253
|
+
className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors"
|
|
254
|
+
>
|
|
255
|
+
{t.trash.overwrite}
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
ArrowLeft, Activity, Globe, Key, Loader2, Server, Search,
|
|
7
|
+
Shield, ShieldCheck, Trash2, Wifi, WifiOff, Zap,
|
|
8
|
+
} from 'lucide-react';
|
|
7
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
10
|
import { toast } from '@/lib/toast';
|
|
9
11
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
@@ -11,6 +13,7 @@ import { useA2aRegistry } from '@/hooks/useA2aRegistry';
|
|
|
11
13
|
import { apiFetch } from '@/lib/api';
|
|
12
14
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
13
15
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
16
|
+
import type { AgentInfo, McpStatus } from '../settings/types';
|
|
14
17
|
import {
|
|
15
18
|
aggregateCrossAgentMcpServers,
|
|
16
19
|
aggregateCrossAgentSkills,
|
|
@@ -43,7 +46,6 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
43
46
|
const [confirmMcpRemove, setConfirmMcpRemove] = useState<string | null>(null);
|
|
44
47
|
const [mcpHint, setMcpHint] = useState<string | null>(null);
|
|
45
48
|
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
|
46
|
-
const [a2aOpen, setA2aOpen] = useState(false);
|
|
47
49
|
|
|
48
50
|
const filteredSkills = useMemo(
|
|
49
51
|
() =>
|
|
@@ -86,7 +88,11 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
86
88
|
() => agent ? generateSnippet(agent, mcp.status, currentTransport) : { snippet: '', path: '' },
|
|
87
89
|
[agent, mcp.status, currentTransport],
|
|
88
90
|
);
|
|
89
|
-
const
|
|
91
|
+
const mindosSkillNames = useMemo(() => new Set(mcp.skills.map((s) => s.name)), [mcp.skills]);
|
|
92
|
+
const nativeInstalledSkills = useMemo(
|
|
93
|
+
() => (agent?.installedSkillNames ?? []).filter((n) => !mindosSkillNames.has(n)),
|
|
94
|
+
[agent?.installedSkillNames, mindosSkillNames],
|
|
95
|
+
);
|
|
90
96
|
const configuredMcpServers = agent?.configuredMcpServers ?? [];
|
|
91
97
|
|
|
92
98
|
|
|
@@ -361,48 +367,6 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
361
367
|
</div>
|
|
362
368
|
</section>
|
|
363
369
|
|
|
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
|
-
|
|
406
370
|
{/* ═══════════ SKILL ASSIGNMENTS ═══════════ */}
|
|
407
371
|
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
408
372
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
@@ -518,7 +482,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
518
482
|
<p className="text-sm text-muted-foreground">{a.detail.noSkills}</p>
|
|
519
483
|
)}
|
|
520
484
|
|
|
521
|
-
{/* Native installed skills
|
|
485
|
+
{/* Native installed skills */}
|
|
522
486
|
{nativeInstalledSkills.length > 0 && (
|
|
523
487
|
<div>
|
|
524
488
|
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
@@ -543,6 +507,59 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
543
507
|
{editError && <p className="text-xs text-error">{editError}</p>}
|
|
544
508
|
</section>
|
|
545
509
|
|
|
510
|
+
{/* ═══════════ A2A CAPABILITIES ═══════════ */}
|
|
511
|
+
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
512
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
513
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
514
|
+
<Globe size={12} className="text-muted-foreground/50" />
|
|
515
|
+
{p.a2aCapabilities}
|
|
516
|
+
</h2>
|
|
517
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${
|
|
518
|
+
status === 'connected' ? 'bg-[var(--success)]/15 text-[var(--success)]' : 'bg-muted text-muted-foreground/60'
|
|
519
|
+
}`}>
|
|
520
|
+
{status === 'connected' ? p.a2aConnected : p.a2aUnavailable}
|
|
521
|
+
</span>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1 py-2 border-y border-border/30">
|
|
525
|
+
<DetailLine label={p.a2aStatus} value={status === 'connected' ? p.a2aConnected : p.a2aUnavailable} />
|
|
526
|
+
<DetailLine label={a.detail.transport} value={agent.transport ?? agent.preferredTransport} />
|
|
527
|
+
{a2a.agents.length > 0 && (
|
|
528
|
+
<DetailLine label="Remote agents" value={String(a2a.agents.length)} />
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{a2a.agents.length > 0 ? (
|
|
533
|
+
<div className="space-y-1">
|
|
534
|
+
{a2a.agents.map((remote) => (
|
|
535
|
+
<div key={remote.id} className="flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-muted/30 transition-colors duration-100">
|
|
536
|
+
<div className="w-6 h-6 rounded-md bg-muted/40 flex items-center justify-center shrink-0">
|
|
537
|
+
<Globe size={11} className="text-muted-foreground/60" />
|
|
538
|
+
</div>
|
|
539
|
+
<span className="text-xs text-foreground flex-1 min-w-0 truncate">{remote.card.name}</span>
|
|
540
|
+
{remote.reachable ? (
|
|
541
|
+
<Wifi size={11} className="text-[var(--success)] shrink-0" />
|
|
542
|
+
) : (
|
|
543
|
+
<WifiOff size={11} className="text-muted-foreground/50 shrink-0" />
|
|
544
|
+
)}
|
|
545
|
+
<span className="text-2xs text-muted-foreground/50 tabular-nums shrink-0">{remote.card.skills.length} skills</span>
|
|
546
|
+
</div>
|
|
547
|
+
))}
|
|
548
|
+
</div>
|
|
549
|
+
) : (
|
|
550
|
+
<p className="text-2xs text-muted-foreground/50">{p.a2aNoRemoteHint}</p>
|
|
551
|
+
)}
|
|
552
|
+
</section>
|
|
553
|
+
|
|
554
|
+
{/* ═══════════ RUNTIME & DIAGNOSTICS ═══════════ */}
|
|
555
|
+
<RuntimeDiagSection agent={agent} status={status} isMindOS={isMindOS} mcpStatus={mcp.status} />
|
|
556
|
+
|
|
557
|
+
{/* ═══════════ ENVIRONMENT & PERMISSIONS ═══════════ */}
|
|
558
|
+
<EnvPermSection agent={agent} isMindOS={isMindOS} />
|
|
559
|
+
|
|
560
|
+
{/* ═══════════ ACTIVITY & USAGE ═══════════ */}
|
|
561
|
+
<ActivitySection agent={agent} />
|
|
562
|
+
|
|
546
563
|
{/* ═══════════ Confirm Dialogs ═══════════ */}
|
|
547
564
|
<ConfirmDialog
|
|
548
565
|
open={confirmDelete !== null}
|
|
@@ -609,3 +626,202 @@ function formatRelativeTime(iso: string | undefined | null): string {
|
|
|
609
626
|
return iso;
|
|
610
627
|
}
|
|
611
628
|
}
|
|
629
|
+
|
|
630
|
+
/* ═══════════ Runtime & Diagnostics ═══════════ */
|
|
631
|
+
|
|
632
|
+
function RuntimeDiagSection({
|
|
633
|
+
agent,
|
|
634
|
+
status,
|
|
635
|
+
isMindOS,
|
|
636
|
+
mcpStatus,
|
|
637
|
+
}: {
|
|
638
|
+
agent: AgentInfo;
|
|
639
|
+
status: string;
|
|
640
|
+
isMindOS: boolean;
|
|
641
|
+
mcpStatus: McpStatus | null;
|
|
642
|
+
}) {
|
|
643
|
+
const { t } = useLocale();
|
|
644
|
+
const d = t.agentsContent.detail;
|
|
645
|
+
const [pingState, setPingState] = useState<'idle' | 'pinging' | 'ok' | 'fail'>('idle');
|
|
646
|
+
const [pingMs, setPingMs] = useState(0);
|
|
647
|
+
|
|
648
|
+
const handlePing = useCallback(async () => {
|
|
649
|
+
setPingState('pinging');
|
|
650
|
+
const start = performance.now();
|
|
651
|
+
try {
|
|
652
|
+
const res = await fetch('/api/mcp/status', { method: 'GET', signal: AbortSignal.timeout(5000) });
|
|
653
|
+
const elapsed = Math.round(performance.now() - start);
|
|
654
|
+
setPingMs(elapsed);
|
|
655
|
+
setPingState(res.ok ? 'ok' : 'fail');
|
|
656
|
+
} catch {
|
|
657
|
+
setPingState('fail');
|
|
658
|
+
}
|
|
659
|
+
}, []);
|
|
660
|
+
|
|
661
|
+
return (
|
|
662
|
+
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
663
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
664
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
665
|
+
<Activity size={12} className="text-muted-foreground/50" />
|
|
666
|
+
{d.runtimeDiagTitle}
|
|
667
|
+
</h2>
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
670
|
+
onClick={() => void handlePing()}
|
|
671
|
+
disabled={pingState === 'pinging'}
|
|
672
|
+
className="inline-flex items-center gap-1 px-2.5 py-1 text-2xs font-medium rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
673
|
+
>
|
|
674
|
+
{pingState === 'pinging' ? (
|
|
675
|
+
<><Loader2 size={10} className="animate-spin" /> {d.runtimePinging}</>
|
|
676
|
+
) : (
|
|
677
|
+
<><Wifi size={10} /> {d.runtimePing}</>
|
|
678
|
+
)}
|
|
679
|
+
</button>
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
{pingState === 'ok' && (
|
|
683
|
+
<div role="status" className="rounded-md bg-[var(--success)]/10 border border-[var(--success)]/20 px-3 py-1.5 text-2xs text-[var(--success)] font-medium animate-in fade-in duration-200">
|
|
684
|
+
{d.runtimePingOk(pingMs)}
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
{pingState === 'fail' && (
|
|
688
|
+
<div role="status" className="rounded-md bg-error/10 border border-error/20 px-3 py-1.5 text-2xs text-error font-medium animate-in fade-in duration-200">
|
|
689
|
+
{d.runtimePingFail}
|
|
690
|
+
</div>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1 py-2 border-y border-border/30">
|
|
694
|
+
<DetailLine label={d.status} value={status} />
|
|
695
|
+
<DetailLine label={d.transport} value={agent.transport ?? agent.preferredTransport} />
|
|
696
|
+
{isMindOS && mcpStatus && (
|
|
697
|
+
<>
|
|
698
|
+
<DetailLine label={d.runtimeVersion} value={mcpStatus.endpoint} />
|
|
699
|
+
<DetailLine label={d.port} value={String(mcpStatus.port)} />
|
|
700
|
+
</>
|
|
701
|
+
)}
|
|
702
|
+
<DetailLine label={d.lastActivityAt} value={formatRelativeTime(agent.runtimeLastActivityAt)} />
|
|
703
|
+
{agent.runtimeConversationSignal !== undefined && (
|
|
704
|
+
<DetailLine label={d.conversationSignal} value={agent.runtimeConversationSignal ? 'Active' : 'Inactive'} />
|
|
705
|
+
)}
|
|
706
|
+
{agent.runtimeUsageSignal !== undefined && (
|
|
707
|
+
<DetailLine label={d.usageSignal} value={agent.runtimeUsageSignal ? 'Active' : 'Inactive'} />
|
|
708
|
+
)}
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
{!agent.runtimeLastActivityAt && pingState === 'idle' && (
|
|
712
|
+
<p className="text-2xs text-muted-foreground/50">{d.runtimeNoData}</p>
|
|
713
|
+
)}
|
|
714
|
+
</section>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* ═══════════ Environment & Permissions ═══════════ */
|
|
719
|
+
|
|
720
|
+
function EnvPermSection({
|
|
721
|
+
agent,
|
|
722
|
+
isMindOS,
|
|
723
|
+
}: {
|
|
724
|
+
agent: AgentInfo;
|
|
725
|
+
isMindOS: boolean;
|
|
726
|
+
}) {
|
|
727
|
+
const { t } = useLocale();
|
|
728
|
+
const d = t.agentsContent.detail;
|
|
729
|
+
|
|
730
|
+
const scope = agent.scope ?? (agent.hasProjectScope ? 'project' : agent.hasGlobalScope ? 'global' : '—');
|
|
731
|
+
const hasHiddenRoot = agent.hiddenRootPresent ?? false;
|
|
732
|
+
|
|
733
|
+
const permissions = [
|
|
734
|
+
{ label: d.envFileAccess, allowed: true },
|
|
735
|
+
{ label: d.envNetworkAccess, allowed: agent.transport === 'http' || isMindOS },
|
|
736
|
+
{ label: d.envWriteAccess, allowed: true },
|
|
737
|
+
{ label: d.envReadOnly, allowed: false },
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
742
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
743
|
+
<Key size={12} className="text-muted-foreground/50" />
|
|
744
|
+
{d.envPermTitle}
|
|
745
|
+
</h2>
|
|
746
|
+
|
|
747
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1 py-2 border-y border-border/30">
|
|
748
|
+
<DetailLine label={d.envScope} value={scope} />
|
|
749
|
+
<DetailLine label={d.format} value={agent.format} />
|
|
750
|
+
{agent.hiddenRootPath && (
|
|
751
|
+
<DetailLine label={d.hiddenRoot} value={agent.hiddenRootPath} />
|
|
752
|
+
)}
|
|
753
|
+
<DetailLine label={d.skillMode} value={agent.skillMode ?? '—'} />
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
<div className="space-y-1">
|
|
757
|
+
<p className="text-2xs font-medium text-muted-foreground/60 uppercase tracking-wider mb-1.5">
|
|
758
|
+
{d.envVars}
|
|
759
|
+
</p>
|
|
760
|
+
{agent.configuredMcpServers && agent.configuredMcpServers.length > 0 ? (
|
|
761
|
+
<p className="text-2xs text-muted-foreground">{d.envVarsCount(agent.configuredMcpServers.length)}</p>
|
|
762
|
+
) : (
|
|
763
|
+
<p className="text-2xs text-muted-foreground/50">{d.envVarsEmpty}</p>
|
|
764
|
+
)}
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div className="space-y-1.5">
|
|
768
|
+
<p className="text-2xs font-medium text-muted-foreground/60 uppercase tracking-wider">Permissions</p>
|
|
769
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
770
|
+
{permissions.map(({ label, allowed }) => (
|
|
771
|
+
<div key={label} className="flex items-center gap-1.5 rounded-md px-2 py-1.5 bg-muted/20">
|
|
772
|
+
{allowed ? (
|
|
773
|
+
<ShieldCheck size={12} className="text-[var(--success)] shrink-0" />
|
|
774
|
+
) : (
|
|
775
|
+
<Shield size={12} className="text-muted-foreground/40 shrink-0" />
|
|
776
|
+
)}
|
|
777
|
+
<span className="text-2xs text-foreground/80">{label}</span>
|
|
778
|
+
<span className={`ml-auto text-2xs font-medium ${allowed ? 'text-[var(--success)]' : 'text-muted-foreground/50'}`}>
|
|
779
|
+
{allowed ? d.envAllowed : d.envRestricted}
|
|
780
|
+
</span>
|
|
781
|
+
</div>
|
|
782
|
+
))}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
</section>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* ═══════════ Activity & Usage ═══════════ */
|
|
790
|
+
|
|
791
|
+
function ActivitySection({ agent }: { agent: AgentInfo }) {
|
|
792
|
+
const { t } = useLocale();
|
|
793
|
+
const d = t.agentsContent.detail;
|
|
794
|
+
|
|
795
|
+
const hasActivity = !!agent.runtimeLastActivityAt;
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
799
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
800
|
+
<Activity size={12} className="text-muted-foreground/50" />
|
|
801
|
+
{d.activityTitle}
|
|
802
|
+
</h2>
|
|
803
|
+
|
|
804
|
+
{hasActivity ? (
|
|
805
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
806
|
+
<StatCard label={d.activityLastInvocation} value={formatRelativeTime(agent.runtimeLastActivityAt)} />
|
|
807
|
+
<StatCard label={d.activityTotal} value={agent.runtimeConversationSignal ? 'Active' : '—'} />
|
|
808
|
+
<StatCard label={d.activityLast7d} value={agent.runtimeUsageSignal ? 'Active' : '—'} />
|
|
809
|
+
</div>
|
|
810
|
+
) : (
|
|
811
|
+
<div className="rounded-lg border border-dashed border-border/50 bg-muted/10 px-4 py-6 text-center">
|
|
812
|
+
<Activity size={20} className="text-muted-foreground/30 mx-auto mb-2" />
|
|
813
|
+
<p className="text-2xs text-muted-foreground/50">{d.activityNoData}</p>
|
|
814
|
+
</div>
|
|
815
|
+
)}
|
|
816
|
+
</section>
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function StatCard({ label, value }: { label: string; value: string }) {
|
|
821
|
+
return (
|
|
822
|
+
<div className="rounded-lg border border-border/40 bg-muted/10 px-3 py-2.5">
|
|
823
|
+
<p className="text-2xs text-muted-foreground/50 uppercase tracking-wider mb-0.5">{label}</p>
|
|
824
|
+
<p className="text-sm font-medium text-foreground font-mono tabular-nums">{value}</p>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
@@ -16,6 +16,7 @@ import AgentsOverviewSection from './AgentsOverviewSection';
|
|
|
16
16
|
import AgentsMcpSection from './AgentsMcpSection';
|
|
17
17
|
import AgentsSkillsSection from './AgentsSkillsSection';
|
|
18
18
|
import AgentsPanelA2aTab from './AgentsPanelA2aTab';
|
|
19
|
+
import AgentsPanelSessionsTab from './AgentsPanelSessionsTab';
|
|
19
20
|
|
|
20
21
|
export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab }) {
|
|
21
22
|
const { t } = useLocale();
|
|
@@ -23,6 +24,12 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
23
24
|
const mcp = useMcpData();
|
|
24
25
|
const a2a = useA2aRegistry();
|
|
25
26
|
const pageHeader = useMemo(() => {
|
|
27
|
+
if (tab === 'sessions') {
|
|
28
|
+
return {
|
|
29
|
+
title: 'Sessions',
|
|
30
|
+
subtitle: 'Active ACP agent sessions.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
26
33
|
if (tab === 'a2a') {
|
|
27
34
|
return {
|
|
28
35
|
title: a.navNetwork,
|
|
@@ -114,6 +121,10 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
114
121
|
onRemove={a2a.remove}
|
|
115
122
|
/>
|
|
116
123
|
)}
|
|
124
|
+
|
|
125
|
+
{tab === 'sessions' && (
|
|
126
|
+
<AgentsPanelSessionsTab />
|
|
127
|
+
)}
|
|
117
128
|
</div>
|
|
118
129
|
);
|
|
119
130
|
}
|