@geminilight/mindos 0.5.63 → 0.5.64
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/changes/route.ts +7 -1
- package/app/app/api/mcp/install-skill/route.ts +9 -24
- package/app/app/layout.tsx +1 -0
- package/app/app/page.tsx +1 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
- package/app/components/HomeContent.tsx +41 -6
- package/app/components/RightAgentDetailPanel.tsx +1 -0
- package/app/components/SidebarLayout.tsx +1 -0
- package/app/components/agents/AgentsContentPage.tsx +20 -16
- package/app/components/agents/AgentsMcpSection.tsx +178 -65
- package/app/components/agents/AgentsOverviewSection.tsx +1 -1
- package/app/components/agents/AgentsSkillsSection.tsx +78 -55
- package/app/components/agents/agents-content-model.ts +16 -0
- package/app/components/changes/ChangesBanner.tsx +90 -13
- package/app/components/changes/ChangesContentPage.tsx +134 -51
- package/app/components/panels/AgentsPanel.tsx +14 -28
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -4
- package/app/components/panels/AgentsPanelAgentGroups.tsx +5 -6
- package/app/components/panels/AgentsPanelAgentListRow.tsx +30 -5
- package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
- package/app/components/panels/PluginsPanel.tsx +3 -3
- package/app/components/renderers/agent-inspector/manifest.ts +2 -0
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/settings/PluginsTab.tsx +4 -3
- package/app/hooks/useMcpData.tsx +3 -2
- package/app/lib/core/content-changes.ts +148 -8
- package/app/lib/fs.ts +7 -1
- package/app/lib/i18n-en.ts +58 -3
- package/app/lib/i18n-zh.ts +58 -3
- package/app/lib/mcp-agents.ts +42 -0
- package/app/lib/renderers/index.ts +1 -2
- package/app/lib/renderers/registry.ts +10 -0
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-agents.js +38 -13
- package/package.json +1 -1
- package/scripts/migrate-agent-diff.js +146 -0
- package/scripts/setup.js +12 -17
- package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
- package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
- package/app/components/renderers/diff/manifest.ts +0 -14
|
@@ -22,10 +22,16 @@ export async function GET(req: NextRequest) {
|
|
|
22
22
|
|
|
23
23
|
if (op === 'list') {
|
|
24
24
|
const path = req.nextUrl.searchParams.get('path') ?? undefined;
|
|
25
|
+
const sourceParam = req.nextUrl.searchParams.get('source');
|
|
26
|
+
const source = sourceParam === 'user' || sourceParam === 'agent' || sourceParam === 'system'
|
|
27
|
+
? sourceParam
|
|
28
|
+
: undefined;
|
|
29
|
+
const opFilter = req.nextUrl.searchParams.get('event_op') ?? undefined;
|
|
30
|
+
const q = req.nextUrl.searchParams.get('q') ?? undefined;
|
|
25
31
|
const limitParam = req.nextUrl.searchParams.get('limit');
|
|
26
32
|
const limit = limitParam ? Number(limitParam) : 50;
|
|
27
33
|
if (!Number.isFinite(limit) || limit <= 0) return err('invalid limit');
|
|
28
|
-
return NextResponse.json({ events: listContentChanges({ path, limit }) });
|
|
34
|
+
return NextResponse.json({ events: listContentChanges({ path, source, op: opFilter, q, limit }) });
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
return err(`unknown op: ${op}`);
|
|
@@ -3,35 +3,15 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import { SKILL_AGENT_REGISTRY } from '@/lib/mcp-agents';
|
|
6
7
|
|
|
7
8
|
/* ── Constants ────────────────────────────────────────────────── */
|
|
8
9
|
|
|
9
10
|
const GITHUB_SOURCE = 'GeminiLight/MindOS';
|
|
10
11
|
|
|
11
|
-
// Universal agents read directly from ~/.agents/skills/ — no symlink needed.
|
|
12
|
-
const UNIVERSAL_AGENTS = new Set([
|
|
13
|
-
'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
|
|
14
|
-
'github-copilot', 'kimi-cli', 'opencode', 'warp',
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
12
|
// Agents that do NOT support Skills at all
|
|
18
13
|
const SKILL_UNSUPPORTED = new Set<string>([]);
|
|
19
14
|
|
|
20
|
-
// MCP agent key → npx skills agent name (for non-universal agents)
|
|
21
|
-
const AGENT_NAME_MAP: Record<string, string> = {
|
|
22
|
-
'claude-code': 'claude-code',
|
|
23
|
-
'windsurf': 'windsurf',
|
|
24
|
-
'trae': 'trae',
|
|
25
|
-
'openclaw': 'openclaw',
|
|
26
|
-
'codebuddy': 'codebuddy',
|
|
27
|
-
'iflow-cli': 'iflow-cli',
|
|
28
|
-
'pi': 'pi',
|
|
29
|
-
'augment': 'augment',
|
|
30
|
-
'qwen-code': 'qwen-code',
|
|
31
|
-
'trae-cn': 'trae-cn',
|
|
32
|
-
'roo': 'roo',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
15
|
/* ── Helpers ──────────────────────────────────────────────────── */
|
|
36
16
|
|
|
37
17
|
/** Fallback: find local skills directory for offline installs */
|
|
@@ -78,9 +58,14 @@ export async function POST(req: NextRequest) {
|
|
|
78
58
|
return NextResponse.json({ error: 'Invalid skill name' }, { status: 400 });
|
|
79
59
|
}
|
|
80
60
|
|
|
81
|
-
const additionalAgents = (agents || [])
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
const additionalAgents = (agents || []).flatMap((key) => {
|
|
62
|
+
if (SKILL_UNSUPPORTED.has(key)) return [];
|
|
63
|
+
const reg = SKILL_AGENT_REGISTRY[key];
|
|
64
|
+
if (!reg) return [key]; // Forward-compatible fallback for unknown keys.
|
|
65
|
+
if (reg.mode === 'unsupported') return [];
|
|
66
|
+
if (reg.mode === 'universal') return [];
|
|
67
|
+
return [reg.skillAgentName || key];
|
|
68
|
+
});
|
|
84
69
|
|
|
85
70
|
// Try GitHub source first, fall back to local path
|
|
86
71
|
const sources = [GITHUB_SOURCE];
|
package/app/app/layout.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import UpdateBanner from '@/components/UpdateBanner';
|
|
|
11
11
|
import UpdateOverlay from '@/components/UpdateOverlay';
|
|
12
12
|
import { cookies } from 'next/headers';
|
|
13
13
|
import type { Locale } from '@/lib/i18n';
|
|
14
|
+
import '@/lib/renderers/index'; // globally register built-in renderers once
|
|
14
15
|
|
|
15
16
|
const geistSans = Inter({
|
|
16
17
|
variable: '--font-geist-sans',
|
package/app/app/page.tsx
CHANGED
|
@@ -2,7 +2,6 @@ import { redirect } from 'next/navigation';
|
|
|
2
2
|
import { readSettings } from '@/lib/settings';
|
|
3
3
|
import { getRecentlyModified, getFileContent, getFileTree } from '@/lib/fs';
|
|
4
4
|
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
5
|
-
import '@/lib/renderers/index'; // registers all renderers
|
|
6
5
|
import HomeContent from '@/components/HomeContent';
|
|
7
6
|
import type { FileNode } from '@/lib/core/types';
|
|
8
7
|
|
|
@@ -83,7 +82,7 @@ export default function HomePage() {
|
|
|
83
82
|
console.error('[HomePage] Failed to load recent files:', err);
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
// Derive
|
|
85
|
+
// Derive renderer entry paths from registry — used by plugin and app-builtin sections on home.
|
|
87
86
|
const entryPaths = getAllRenderers()
|
|
88
87
|
.map(r => r.entryPath)
|
|
89
88
|
.filter((p): p is string => !!p);
|
|
@@ -17,7 +17,6 @@ import { resolveRenderer, isRendererEnabled } from '@/lib/renderers/registry';
|
|
|
17
17
|
import { encodePath } from '@/lib/utils';
|
|
18
18
|
import { useLocale } from '@/lib/LocaleContext';
|
|
19
19
|
import DirPicker from '@/components/DirPicker';
|
|
20
|
-
import '@/lib/renderers/index'; // registers all renderers
|
|
21
20
|
|
|
22
21
|
interface ViewPageClientProps {
|
|
23
22
|
filePath: string;
|
|
@@ -5,8 +5,7 @@ import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, Chevron
|
|
|
5
5
|
import { useState, useEffect, useMemo } from 'react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
|
|
8
|
-
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
9
|
-
import '@/lib/renderers/index'; // registers all renderers
|
|
8
|
+
import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
|
|
10
9
|
import OnboardingView from './OnboardingView';
|
|
11
10
|
import GuideCard from './GuideCard';
|
|
12
11
|
import CreateSpaceModal from './CreateSpaceModal';
|
|
@@ -124,8 +123,10 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
124
123
|
|
|
125
124
|
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
126
125
|
|
|
127
|
-
//
|
|
128
|
-
const availablePlugins =
|
|
126
|
+
// User-manageable plugins: only show available entry files
|
|
127
|
+
const availablePlugins = getPluginRenderers().filter(r => r.entryPath && existingSet.has(r.entryPath));
|
|
128
|
+
// App-builtin features: always visible, with active/inactive state
|
|
129
|
+
const builtinFeatures = getAllRenderers().filter((r) => r.appBuiltinFeature && r.id !== 'csv');
|
|
129
130
|
|
|
130
131
|
const lastFile = recent[0];
|
|
131
132
|
|
|
@@ -275,7 +276,41 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
275
276
|
<CreateSpaceModal t={t} dirPaths={dirPaths ?? []} />
|
|
276
277
|
</section>
|
|
277
278
|
|
|
278
|
-
{/* ── Section 2:
|
|
279
|
+
{/* ── Section 2: Built-in capabilities ── */}
|
|
280
|
+
{builtinFeatures.length > 0 && (
|
|
281
|
+
<section className="mb-8">
|
|
282
|
+
<SectionTitle icon={<Puzzle size={13} />} count={builtinFeatures.length}>
|
|
283
|
+
{t.home.builtinFeatures}
|
|
284
|
+
</SectionTitle>
|
|
285
|
+
<div className="flex flex-wrap gap-2">
|
|
286
|
+
{builtinFeatures.map((r) => {
|
|
287
|
+
const active = !!r.entryPath && existingSet.has(r.entryPath);
|
|
288
|
+
if (active && r.entryPath) {
|
|
289
|
+
return (
|
|
290
|
+
<Link key={r.id} href={`/view/${encodePath(r.entryPath)}`}>
|
|
291
|
+
<span className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border text-xs transition-all duration-150 hover:border-amber-500/30 hover:bg-muted/60">
|
|
292
|
+
<span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
|
|
293
|
+
<span className="font-medium text-foreground">{r.name}</span>
|
|
294
|
+
</span>
|
|
295
|
+
</Link>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return (
|
|
299
|
+
<span
|
|
300
|
+
key={r.id}
|
|
301
|
+
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-dashed border-border text-xs text-muted-foreground opacity-70"
|
|
302
|
+
title={r.entryPath ? t.home.createToActivate.replace('{file}', r.entryPath) : t.home.builtinInactive}
|
|
303
|
+
>
|
|
304
|
+
<span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
|
|
305
|
+
<span className="font-medium">{r.name}</span>
|
|
306
|
+
</span>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
</section>
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{/* ── Section 3: Extensions ── */}
|
|
279
314
|
{availablePlugins.length > 0 && (
|
|
280
315
|
<section className="mb-8">
|
|
281
316
|
<SectionTitle
|
|
@@ -310,7 +345,7 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
310
345
|
</section>
|
|
311
346
|
)}
|
|
312
347
|
|
|
313
|
-
{/* ── Section
|
|
348
|
+
{/* ── Section 4: Recently Edited ── */}
|
|
314
349
|
{recent.length > 0 && (
|
|
315
350
|
<section className="mb-12">
|
|
316
351
|
<SectionTitle icon={<Clock size={13} />} count={recent.length}>{t.home.recentlyEdited}</SectionTitle>
|
|
@@ -31,6 +31,7 @@ import { FileNode } from '@/lib/types';
|
|
|
31
31
|
import { useLocale } from '@/lib/LocaleContext';
|
|
32
32
|
import { WalkthroughProvider } from './walkthrough';
|
|
33
33
|
import McpProvider from '@/hooks/useMcpData';
|
|
34
|
+
import '@/lib/renderers/index'; // client-side renderer registration source of truth
|
|
34
35
|
import { useLeftPanel } from '@/hooks/useLeftPanel';
|
|
35
36
|
import { useAskPanel } from '@/hooks/useAskPanel';
|
|
36
37
|
import type { Tab } from './settings/types';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
|
-
import Link from 'next/link';
|
|
5
4
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
5
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
7
6
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
@@ -20,6 +19,24 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
20
19
|
const a = t.agentsContent;
|
|
21
20
|
const mcp = useMcpData();
|
|
22
21
|
const [copyState, setCopyState] = useState<string | null>(null);
|
|
22
|
+
const pageHeader = useMemo(() => {
|
|
23
|
+
if (tab === 'skills') {
|
|
24
|
+
return {
|
|
25
|
+
title: a.navSkills,
|
|
26
|
+
subtitle: a.skills.capabilityGroups,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (tab === 'mcp') {
|
|
30
|
+
return {
|
|
31
|
+
title: a.navMcp,
|
|
32
|
+
subtitle: a.mcp.connectionGraph,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
title: a.title,
|
|
37
|
+
subtitle: a.subtitle,
|
|
38
|
+
};
|
|
39
|
+
}, [a, tab]);
|
|
23
40
|
|
|
24
41
|
const buckets = useMemo(() => bucketAgents(mcp.agents), [mcp.agents]);
|
|
25
42
|
const riskQueue = useMemo(
|
|
@@ -33,13 +50,6 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
33
50
|
[mcp.skills, mcp.status?.running, buckets.detected.length, buckets.notFound.length],
|
|
34
51
|
);
|
|
35
52
|
|
|
36
|
-
const navClass = (target: AgentsDashboardTab) =>
|
|
37
|
-
`px-3 py-1.5 text-xs rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
38
|
-
tab === target
|
|
39
|
-
? 'border-border bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
40
|
-
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
41
|
-
}`;
|
|
42
|
-
|
|
43
53
|
const copySnippet = async (agentKey: string) => {
|
|
44
54
|
const agent = mcp.agents.find((item) => item.key === agentKey);
|
|
45
55
|
if (!agent) return;
|
|
@@ -53,16 +63,10 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
|
|
|
53
63
|
return (
|
|
54
64
|
<div className="content-width px-4 md:px-6 py-8 md:py-10">
|
|
55
65
|
<header className="mb-6">
|
|
56
|
-
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">{
|
|
57
|
-
<p className="mt-1 text-sm text-muted-foreground">{
|
|
66
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">{pageHeader.title}</h1>
|
|
67
|
+
<p className="mt-1 text-sm text-muted-foreground">{pageHeader.subtitle}</p>
|
|
58
68
|
</header>
|
|
59
69
|
|
|
60
|
-
<div className="mb-6 flex items-center gap-2 border-b border-border pb-3" role="tablist" aria-label={a.title}>
|
|
61
|
-
<Link href="/agents" role="tab" id="agents-tab-overview" aria-controls="agents-panel-overview" aria-selected={tab === 'overview'} className={navClass('overview')}>{a.navOverview}</Link>
|
|
62
|
-
<Link href="/agents?tab=mcp" role="tab" id="agents-tab-mcp" aria-controls="agents-panel-mcp" aria-selected={tab === 'mcp'} className={navClass('mcp')}>{a.navMcp}</Link>
|
|
63
|
-
<Link href="/agents?tab=skills" role="tab" id="agents-tab-skills" aria-controls="agents-panel-skills" aria-selected={tab === 'skills'} className={navClass('skills')}>{a.navSkills}</Link>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
70
|
{tab === 'overview' && (
|
|
67
71
|
<AgentsOverviewSection
|
|
68
72
|
copy={a.overview}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
3
4
|
import Link from 'next/link';
|
|
4
|
-
import { RefreshCw, Server } from 'lucide-react';
|
|
5
|
+
import { RefreshCw, Search, Server } from 'lucide-react';
|
|
5
6
|
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
6
7
|
import type { AgentBuckets } from './agents-content-model';
|
|
8
|
+
import type { AgentStatusFilter } from './agents-content-model';
|
|
9
|
+
import { filterAgentsForMcpTable } from './agents-content-model';
|
|
7
10
|
|
|
8
11
|
export default function AgentsMcpSection({
|
|
9
12
|
copy,
|
|
@@ -16,6 +19,18 @@ export default function AgentsMcpSection({
|
|
|
16
19
|
title: string;
|
|
17
20
|
refresh: string;
|
|
18
21
|
connectionGraph: string;
|
|
22
|
+
tabs: {
|
|
23
|
+
manage: string;
|
|
24
|
+
topology: string;
|
|
25
|
+
};
|
|
26
|
+
searchPlaceholder: string;
|
|
27
|
+
emptyState: string;
|
|
28
|
+
filters: {
|
|
29
|
+
all: string;
|
|
30
|
+
connected: string;
|
|
31
|
+
detected: string;
|
|
32
|
+
notFound: string;
|
|
33
|
+
};
|
|
19
34
|
table: { agent: string; status: string; transport: string; actions: string };
|
|
20
35
|
actions: { copySnippet: string; copied: string; testConnection: string; reconnect: string };
|
|
21
36
|
status: { connected: string; detected: string; notFound: string };
|
|
@@ -25,80 +40,178 @@ export default function AgentsMcpSection({
|
|
|
25
40
|
copyState: string | null;
|
|
26
41
|
onCopySnippet: (agentKey: string) => Promise<void>;
|
|
27
42
|
}) {
|
|
43
|
+
const [query, setQuery] = useState('');
|
|
44
|
+
const [statusFilter, setStatusFilter] = useState<AgentStatusFilter>('all');
|
|
45
|
+
const [busyAction, setBusyAction] = useState<string | null>(null);
|
|
46
|
+
const [view, setView] = useState<'manage' | 'topology'>('manage');
|
|
47
|
+
const filteredAgents = useMemo(
|
|
48
|
+
() => filterAgentsForMcpTable(mcp.agents, query, statusFilter),
|
|
49
|
+
[mcp.agents, query, statusFilter],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
async function handleTestConnection(agentKey: string) {
|
|
53
|
+
setBusyAction(`test:${agentKey}`);
|
|
54
|
+
try {
|
|
55
|
+
await mcp.refresh();
|
|
56
|
+
} finally {
|
|
57
|
+
setBusyAction(null);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleReconnect(agent: typeof mcp.agents[number]) {
|
|
62
|
+
setBusyAction(`reconnect:${agent.key}`);
|
|
63
|
+
try {
|
|
64
|
+
const scope = agent.scope === 'project' ? 'project' : 'global';
|
|
65
|
+
const transport = agent.transport === 'http' ? 'http' : 'stdio';
|
|
66
|
+
await mcp.installAgent(agent.key, { scope, transport });
|
|
67
|
+
await mcp.refresh();
|
|
68
|
+
} finally {
|
|
69
|
+
setBusyAction(null);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
28
73
|
return (
|
|
29
|
-
<section
|
|
74
|
+
<section className="rounded-lg border border-border bg-card p-4 space-y-4">
|
|
30
75
|
<div className="flex items-center justify-between">
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<h2 className="text-sm font-medium text-foreground flex items-center gap-2">
|
|
78
|
+
<Server size={15} className="text-muted-foreground" />
|
|
79
|
+
{copy.title}
|
|
80
|
+
</h2>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => void mcp.refresh()}
|
|
84
|
+
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
85
|
+
>
|
|
86
|
+
<RefreshCw size={13} />
|
|
87
|
+
{copy.refresh}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-1 rounded-md border border-border p-1 bg-background">
|
|
91
|
+
<StatusFilterButton active={view === 'manage'} label={copy.tabs.manage} onClick={() => setView('manage')} />
|
|
92
|
+
<StatusFilterButton active={view === 'topology'} label={copy.tabs.topology} onClick={() => setView('topology')} />
|
|
93
|
+
</div>
|
|
43
94
|
</div>
|
|
44
95
|
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
{view === 'topology' ? (
|
|
97
|
+
<div className="rounded-md border border-border bg-background p-3">
|
|
98
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">{copy.connectionGraph}</p>
|
|
99
|
+
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
100
|
+
<NodePill label={copy.status.connected} count={buckets.connected.length} tone="ok" />
|
|
101
|
+
<span className="text-muted-foreground">→</span>
|
|
102
|
+
<NodePill label={copy.status.detected} count={buckets.detected.length} tone="warn" />
|
|
103
|
+
<span className="text-muted-foreground">→</span>
|
|
104
|
+
<NodePill label={copy.status.notFound} count={buckets.notFound.length} tone="neutral" />
|
|
105
|
+
<span className="mx-2 text-muted-foreground">|</span>
|
|
106
|
+
<NodePill label={copy.title} count={mcp.status?.running ? 1 : 0} tone={mcp.status?.running ? 'ok' : 'neutral'} />
|
|
107
|
+
</div>
|
|
55
108
|
</div>
|
|
56
|
-
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
112
|
+
<label className="relative flex-1">
|
|
113
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
114
|
+
<input
|
|
115
|
+
value={query}
|
|
116
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
117
|
+
placeholder={copy.searchPlaceholder}
|
|
118
|
+
className="w-full h-9 rounded-md border border-border bg-background pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
119
|
+
/>
|
|
120
|
+
</label>
|
|
121
|
+
<div role="group" aria-label={copy.table.status} className="flex items-center gap-1 rounded-md border border-border p-1 bg-background">
|
|
122
|
+
<StatusFilterButton active={statusFilter === 'all'} label={copy.filters.all} onClick={() => setStatusFilter('all')} />
|
|
123
|
+
<StatusFilterButton active={statusFilter === 'connected'} label={copy.filters.connected} onClick={() => setStatusFilter('connected')} />
|
|
124
|
+
<StatusFilterButton active={statusFilter === 'detected'} label={copy.filters.detected} onClick={() => setStatusFilter('detected')} />
|
|
125
|
+
<StatusFilterButton active={statusFilter === 'notFound'} label={copy.filters.notFound} onClick={() => setStatusFilter('notFound')} />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
57
128
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
<div className="overflow-x-auto">
|
|
130
|
+
<table className="w-full text-sm">
|
|
131
|
+
<thead>
|
|
132
|
+
<tr className="text-left border-b border-border">
|
|
133
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.agent}</th>
|
|
134
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.status}</th>
|
|
135
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.transport}</th>
|
|
136
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.actions}</th>
|
|
137
|
+
</tr>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>
|
|
140
|
+
{filteredAgents.map((agent) => (
|
|
141
|
+
<tr key={agent.key} className="border-b border-border/60">
|
|
142
|
+
<td className="py-2 text-foreground">
|
|
143
|
+
<Link href={`/agents/${encodeURIComponent(agent.key)}`} className="hover:underline">{agent.name}</Link>
|
|
144
|
+
</td>
|
|
145
|
+
<td className="py-2 text-muted-foreground">{agent.present ? (agent.installed ? copy.status.connected : copy.status.detected) : copy.status.notFound}</td>
|
|
146
|
+
<td className="py-2 text-muted-foreground">{agent.transport ?? agent.preferredTransport}</td>
|
|
147
|
+
<td className="py-2">
|
|
148
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => void onCopySnippet(agent.key)}
|
|
152
|
+
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
153
|
+
>
|
|
154
|
+
{copyState === agent.key ? copy.actions.copied : copy.actions.copySnippet}
|
|
155
|
+
</button>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => void handleTestConnection(agent.key)}
|
|
159
|
+
disabled={!agent.installed || busyAction !== null}
|
|
160
|
+
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
|
161
|
+
>
|
|
162
|
+
{copy.actions.testConnection}
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={() => void handleReconnect(agent)}
|
|
167
|
+
disabled={busyAction !== null}
|
|
168
|
+
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
|
169
|
+
>
|
|
170
|
+
{copy.actions.reconnect}
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
))}
|
|
176
|
+
{filteredAgents.length === 0 && (
|
|
177
|
+
<tr>
|
|
178
|
+
<td colSpan={4} className="py-4 text-sm text-muted-foreground text-center">
|
|
179
|
+
{copy.emptyState}
|
|
180
|
+
</td>
|
|
181
|
+
</tr>
|
|
182
|
+
)}
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
|
185
|
+
</div>
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
98
188
|
</section>
|
|
99
189
|
);
|
|
100
190
|
}
|
|
101
191
|
|
|
192
|
+
function StatusFilterButton({
|
|
193
|
+
active,
|
|
194
|
+
label,
|
|
195
|
+
onClick,
|
|
196
|
+
}: {
|
|
197
|
+
active: boolean;
|
|
198
|
+
label: string;
|
|
199
|
+
onClick: () => void;
|
|
200
|
+
}) {
|
|
201
|
+
return (
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={onClick}
|
|
205
|
+
aria-pressed={active}
|
|
206
|
+
className={`px-2.5 h-7 rounded text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
207
|
+
active ? 'bg-[var(--amber-dim)] text-[var(--amber)]' : 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
{label}
|
|
211
|
+
</button>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
102
215
|
function NodePill({
|
|
103
216
|
label,
|
|
104
217
|
count,
|
|
@@ -32,7 +32,7 @@ export default function AgentsOverviewSection({
|
|
|
32
32
|
failedAgentsValue: string;
|
|
33
33
|
}) {
|
|
34
34
|
return (
|
|
35
|
-
<div className="space-y-4"
|
|
35
|
+
<div className="space-y-4">
|
|
36
36
|
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
37
37
|
<StatCard title={copy.connected} value={String(buckets.connected.length)} tone="ok" />
|
|
38
38
|
<StatCard title={copy.detected} value={String(buckets.detected.length)} tone="warn" />
|