@geminilight/mindos 0.5.60 → 0.5.62
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/agents/[agentKey]/page.tsx +17 -0
- package/app/app/agents/page.tsx +20 -0
- package/app/app/api/changes/route.ts +57 -0
- package/app/app/api/file/route.ts +146 -1
- package/app/app/api/settings/test-key/route.ts +43 -1
- package/app/app/changes/page.tsx +16 -0
- package/app/components/ActivityBar.tsx +9 -1
- package/app/components/SidebarLayout.tsx +12 -2
- package/app/components/agents/AgentDetailContent.tsx +117 -0
- package/app/components/agents/AgentsContentPage.tsx +87 -0
- package/app/components/agents/AgentsMcpSection.tsx +123 -0
- package/app/components/agents/AgentsOverviewSection.tsx +89 -0
- package/app/components/agents/AgentsSkillsSection.tsx +146 -0
- package/app/components/agents/agents-content-model.ts +80 -0
- package/app/components/ask/AskContent.tsx +15 -6
- package/app/components/changes/ChangesBanner.tsx +51 -0
- package/app/components/changes/ChangesContentPage.tsx +190 -0
- package/app/components/changes/line-diff.ts +75 -0
- package/app/components/panels/AgentsPanel.tsx +11 -0
- package/app/components/settings/UpdateTab.tsx +190 -1
- package/app/lib/core/content-changes.ts +153 -0
- package/app/lib/core/index.ts +14 -0
- package/app/lib/fs.ts +22 -0
- package/app/lib/i18n-en.ts +92 -0
- package/app/lib/i18n-zh.ts +92 -0
- package/app/lib/renderers/index.ts +1 -2
- package/app/package.json +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { useMcpData } from '@/hooks/useMcpData';
|
|
7
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
8
|
+
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
9
|
+
import {
|
|
10
|
+
bucketAgents,
|
|
11
|
+
buildRiskQueue,
|
|
12
|
+
type AgentsDashboardTab,
|
|
13
|
+
} from './agents-content-model';
|
|
14
|
+
import AgentsOverviewSection from './AgentsOverviewSection';
|
|
15
|
+
import AgentsMcpSection from './AgentsMcpSection';
|
|
16
|
+
import AgentsSkillsSection from './AgentsSkillsSection';
|
|
17
|
+
|
|
18
|
+
export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab }) {
|
|
19
|
+
const { t } = useLocale();
|
|
20
|
+
const a = t.agentsContent;
|
|
21
|
+
const mcp = useMcpData();
|
|
22
|
+
const [copyState, setCopyState] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const buckets = useMemo(() => bucketAgents(mcp.agents), [mcp.agents]);
|
|
25
|
+
const riskQueue = useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
buildRiskQueue({
|
|
28
|
+
mcpRunning: !!mcp.status?.running,
|
|
29
|
+
detectedCount: buckets.detected.length,
|
|
30
|
+
notFoundCount: buckets.notFound.length,
|
|
31
|
+
allSkillsDisabled: mcp.skills.length > 0 && mcp.skills.every((s) => !s.enabled),
|
|
32
|
+
}),
|
|
33
|
+
[mcp.skills, mcp.status?.running, buckets.detected.length, buckets.notFound.length],
|
|
34
|
+
);
|
|
35
|
+
|
|
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
|
+
const copySnippet = async (agentKey: string) => {
|
|
44
|
+
const agent = mcp.agents.find((item) => item.key === agentKey);
|
|
45
|
+
if (!agent) return;
|
|
46
|
+
const snippet = generateSnippet(agent, mcp.status, agent.preferredTransport);
|
|
47
|
+
const ok = await copyToClipboard(snippet.snippet);
|
|
48
|
+
if (!ok) return;
|
|
49
|
+
setCopyState(agentKey);
|
|
50
|
+
setTimeout(() => setCopyState(null), 1500);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-10">
|
|
55
|
+
<header className="mb-6">
|
|
56
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">{a.title}</h1>
|
|
57
|
+
<p className="mt-1 text-sm text-muted-foreground">{a.subtitle}</p>
|
|
58
|
+
</header>
|
|
59
|
+
|
|
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
|
+
{tab === 'overview' && (
|
|
67
|
+
<AgentsOverviewSection
|
|
68
|
+
copy={a.overview}
|
|
69
|
+
buckets={buckets}
|
|
70
|
+
riskQueue={riskQueue}
|
|
71
|
+
topSkillsLabel={a.overview.topSkills}
|
|
72
|
+
failedAgentsLabel={a.overview.failedAgents}
|
|
73
|
+
topSkillsValue={mcp.skills.filter((s) => s.enabled).slice(0, 3).map((s) => s.name).join(', ') || a.overview.na}
|
|
74
|
+
failedAgentsValue={buckets.notFound.map((x) => x.name).slice(0, 3).join(', ') || a.overview.na}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{tab === 'mcp' && (
|
|
79
|
+
<AgentsMcpSection copy={{ ...a.mcp, status: a.status }} mcp={mcp} buckets={buckets} copyState={copyState} onCopySnippet={copySnippet} />
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{tab === 'skills' && (
|
|
83
|
+
<AgentsSkillsSection copy={a.skills} mcp={mcp} buckets={buckets} />
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { RefreshCw, Server } from 'lucide-react';
|
|
5
|
+
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
6
|
+
import type { AgentBuckets } from './agents-content-model';
|
|
7
|
+
|
|
8
|
+
export default function AgentsMcpSection({
|
|
9
|
+
copy,
|
|
10
|
+
mcp,
|
|
11
|
+
buckets,
|
|
12
|
+
copyState,
|
|
13
|
+
onCopySnippet,
|
|
14
|
+
}: {
|
|
15
|
+
copy: {
|
|
16
|
+
title: string;
|
|
17
|
+
refresh: string;
|
|
18
|
+
connectionGraph: string;
|
|
19
|
+
table: { agent: string; status: string; transport: string; actions: string };
|
|
20
|
+
actions: { copySnippet: string; copied: string; testConnection: string; reconnect: string };
|
|
21
|
+
status: { connected: string; detected: string; notFound: string };
|
|
22
|
+
};
|
|
23
|
+
mcp: McpContextValue;
|
|
24
|
+
buckets: AgentBuckets;
|
|
25
|
+
copyState: string | null;
|
|
26
|
+
onCopySnippet: (agentKey: string) => Promise<void>;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<section role="tabpanel" id="agents-panel-mcp" aria-labelledby="agents-tab-mcp" className="rounded-lg border border-border bg-card p-4 space-y-4">
|
|
30
|
+
<div className="flex items-center justify-between">
|
|
31
|
+
<h2 className="text-sm font-medium text-foreground flex items-center gap-2">
|
|
32
|
+
<Server size={15} className="text-muted-foreground" />
|
|
33
|
+
{copy.title}
|
|
34
|
+
</h2>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={() => void mcp.refresh()}
|
|
38
|
+
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"
|
|
39
|
+
>
|
|
40
|
+
<RefreshCw size={13} />
|
|
41
|
+
{copy.refresh}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="rounded-md border border-border bg-background p-3">
|
|
46
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">{copy.connectionGraph}</p>
|
|
47
|
+
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
48
|
+
<NodePill label={copy.status.connected} count={buckets.connected.length} tone="ok" />
|
|
49
|
+
<span className="text-muted-foreground">→</span>
|
|
50
|
+
<NodePill label={copy.status.detected} count={buckets.detected.length} tone="warn" />
|
|
51
|
+
<span className="text-muted-foreground">→</span>
|
|
52
|
+
<NodePill label={copy.status.notFound} count={buckets.notFound.length} tone="neutral" />
|
|
53
|
+
<span className="mx-2 text-muted-foreground">|</span>
|
|
54
|
+
<NodePill label={copy.title} count={mcp.status?.running ? 1 : 0} tone={mcp.status?.running ? 'ok' : 'neutral'} />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="overflow-x-auto">
|
|
59
|
+
<table className="w-full text-sm">
|
|
60
|
+
<thead>
|
|
61
|
+
<tr className="text-left border-b border-border">
|
|
62
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.agent}</th>
|
|
63
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.status}</th>
|
|
64
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.transport}</th>
|
|
65
|
+
<th className="py-2 font-medium text-muted-foreground">{copy.table.actions}</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
{mcp.agents.map((agent) => (
|
|
70
|
+
<tr key={agent.key} className="border-b border-border/60">
|
|
71
|
+
<td className="py-2 text-foreground">
|
|
72
|
+
<Link href={`/agents/${encodeURIComponent(agent.key)}`} className="hover:underline">{agent.name}</Link>
|
|
73
|
+
</td>
|
|
74
|
+
<td className="py-2 text-muted-foreground">{agent.present ? (agent.installed ? copy.status.connected : copy.status.detected) : copy.status.notFound}</td>
|
|
75
|
+
<td className="py-2 text-muted-foreground">{agent.transport ?? agent.preferredTransport}</td>
|
|
76
|
+
<td className="py-2">
|
|
77
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => void onCopySnippet(agent.key)}
|
|
81
|
+
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"
|
|
82
|
+
>
|
|
83
|
+
{copyState === agent.key ? copy.actions.copied : copy.actions.copySnippet}
|
|
84
|
+
</button>
|
|
85
|
+
<button type="button" 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">
|
|
86
|
+
{copy.actions.testConnection}
|
|
87
|
+
</button>
|
|
88
|
+
<button type="button" 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">
|
|
89
|
+
{copy.actions.reconnect}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</td>
|
|
93
|
+
</tr>
|
|
94
|
+
))}
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function NodePill({
|
|
103
|
+
label,
|
|
104
|
+
count,
|
|
105
|
+
tone,
|
|
106
|
+
}: {
|
|
107
|
+
label: string;
|
|
108
|
+
count: number;
|
|
109
|
+
tone: 'ok' | 'warn' | 'neutral';
|
|
110
|
+
}) {
|
|
111
|
+
const cls =
|
|
112
|
+
tone === 'ok'
|
|
113
|
+
? 'bg-success/10 text-success'
|
|
114
|
+
: tone === 'warn'
|
|
115
|
+
? 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
116
|
+
: 'bg-muted text-muted-foreground';
|
|
117
|
+
return (
|
|
118
|
+
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md ${cls}`}>
|
|
119
|
+
<span>{label}</span>
|
|
120
|
+
<span className="tabular-nums">{count}</span>
|
|
121
|
+
</span>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle, CheckCircle2, Wrench } from 'lucide-react';
|
|
4
|
+
import type { AgentBuckets, RiskItem } from './agents-content-model';
|
|
5
|
+
|
|
6
|
+
export default function AgentsOverviewSection({
|
|
7
|
+
copy,
|
|
8
|
+
buckets,
|
|
9
|
+
riskQueue,
|
|
10
|
+
topSkillsLabel,
|
|
11
|
+
failedAgentsLabel,
|
|
12
|
+
topSkillsValue,
|
|
13
|
+
failedAgentsValue,
|
|
14
|
+
}: {
|
|
15
|
+
copy: {
|
|
16
|
+
connected: string;
|
|
17
|
+
detected: string;
|
|
18
|
+
notFound: string;
|
|
19
|
+
riskQueue: string;
|
|
20
|
+
noRisk: string;
|
|
21
|
+
usagePulse: string;
|
|
22
|
+
successRate7d: string;
|
|
23
|
+
topSkills: string;
|
|
24
|
+
failedAgents: string;
|
|
25
|
+
na: string;
|
|
26
|
+
};
|
|
27
|
+
buckets: AgentBuckets;
|
|
28
|
+
riskQueue: RiskItem[];
|
|
29
|
+
topSkillsLabel: string;
|
|
30
|
+
failedAgentsLabel: string;
|
|
31
|
+
topSkillsValue: string;
|
|
32
|
+
failedAgentsValue: string;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-4" role="tabpanel" id="agents-panel-overview" aria-labelledby="agents-tab-overview">
|
|
36
|
+
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
37
|
+
<StatCard title={copy.connected} value={String(buckets.connected.length)} tone="ok" />
|
|
38
|
+
<StatCard title={copy.detected} value={String(buckets.detected.length)} tone="warn" />
|
|
39
|
+
<StatCard title={copy.notFound} value={String(buckets.notFound.length)} tone="warn" />
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
43
|
+
<h2 className="text-sm font-medium text-foreground mb-3">{copy.riskQueue}</h2>
|
|
44
|
+
{riskQueue.length === 0 ? (
|
|
45
|
+
<p className="text-sm text-muted-foreground">{copy.noRisk}</p>
|
|
46
|
+
) : (
|
|
47
|
+
<ul className="space-y-2">
|
|
48
|
+
{riskQueue.map((risk) => (
|
|
49
|
+
<li key={risk.id} className="flex items-start gap-2 text-sm">
|
|
50
|
+
<AlertTriangle size={14} className={risk.severity === 'error' ? 'text-destructive mt-0.5' : 'text-[var(--amber)] mt-0.5'} />
|
|
51
|
+
<span className="text-foreground">{risk.title}</span>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
)}
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
59
|
+
<h2 className="text-sm font-medium text-foreground mb-3">{copy.usagePulse}</h2>
|
|
60
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
|
61
|
+
<InfoLine label={copy.successRate7d} value={copy.na} />
|
|
62
|
+
<InfoLine label={topSkillsLabel || copy.topSkills} value={topSkillsValue} />
|
|
63
|
+
<InfoLine label={failedAgentsLabel || copy.failedAgents} value={failedAgentsValue} />
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function StatCard({ title, value, tone }: { title: string; value: string; tone: 'ok' | 'warn' }) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
73
|
+
<div className="flex items-center justify-between mb-2">
|
|
74
|
+
<p className="text-xs text-muted-foreground">{title}</p>
|
|
75
|
+
{tone === 'ok' ? <CheckCircle2 size={14} className="text-success" /> : <Wrench size={14} className="text-[var(--amber)]" />}
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-xl font-semibold text-foreground">{value}</p>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function InfoLine({ label, value }: { label: string; value: string }) {
|
|
83
|
+
return (
|
|
84
|
+
<div className="rounded-md border border-border px-3 py-2">
|
|
85
|
+
<p className="text-2xs text-muted-foreground mb-1">{label}</p>
|
|
86
|
+
<p className="text-sm text-foreground truncate">{value}</p>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
|
|
5
|
+
import { Toggle } from '@/components/settings/Primitives';
|
|
6
|
+
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
7
|
+
import type { AgentBuckets, SkillSourceFilter } from './agents-content-model';
|
|
8
|
+
import { filterSkills, groupSkillsByCapability } from './agents-content-model';
|
|
9
|
+
|
|
10
|
+
export default function AgentsSkillsSection({
|
|
11
|
+
copy,
|
|
12
|
+
mcp,
|
|
13
|
+
buckets,
|
|
14
|
+
}: {
|
|
15
|
+
copy: {
|
|
16
|
+
title: string;
|
|
17
|
+
capabilityGroups: string;
|
|
18
|
+
searchPlaceholder: string;
|
|
19
|
+
sourceAll: string;
|
|
20
|
+
sourceBuiltin: string;
|
|
21
|
+
sourceUser: string;
|
|
22
|
+
emptyGroup: string;
|
|
23
|
+
matrixToggle: string;
|
|
24
|
+
groupLabels: Record<'research' | 'coding' | 'docs' | 'ops' | 'memory', string>;
|
|
25
|
+
};
|
|
26
|
+
mcp: McpContextValue;
|
|
27
|
+
buckets: AgentBuckets;
|
|
28
|
+
}) {
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const [source, setSource] = useState<SkillSourceFilter>('all');
|
|
31
|
+
const [matrixOpen, setMatrixOpen] = useState(false);
|
|
32
|
+
|
|
33
|
+
const filtered = useMemo(() => filterSkills(mcp.skills, query, source), [mcp.skills, query, source]);
|
|
34
|
+
const grouped = useMemo(() => groupSkillsByCapability(filtered), [filtered]);
|
|
35
|
+
const knownAgents = useMemo(() => [...buckets.connected, ...buckets.detected, ...buckets.notFound], [buckets]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section role="tabpanel" id="agents-panel-skills" aria-labelledby="agents-tab-skills" className="rounded-lg border border-border bg-card p-4 space-y-4">
|
|
39
|
+
<div>
|
|
40
|
+
<h2 className="text-sm font-medium text-foreground mb-1">{copy.title}</h2>
|
|
41
|
+
<p className="text-xs text-muted-foreground">{copy.capabilityGroups}</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
45
|
+
<label className="relative flex-1">
|
|
46
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
47
|
+
<input
|
|
48
|
+
value={query}
|
|
49
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
50
|
+
placeholder={copy.searchPlaceholder}
|
|
51
|
+
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"
|
|
52
|
+
/>
|
|
53
|
+
</label>
|
|
54
|
+
<div className="flex items-center gap-1 rounded-md border border-border p-1 bg-background">
|
|
55
|
+
<SourceFilterButton active={source === 'all'} label={copy.sourceAll} onClick={() => setSource('all')} />
|
|
56
|
+
<SourceFilterButton active={source === 'builtin'} label={copy.sourceBuiltin} onClick={() => setSource('builtin')} />
|
|
57
|
+
<SourceFilterButton active={source === 'user'} label={copy.sourceUser} onClick={() => setSource('user')} />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="space-y-4">
|
|
62
|
+
{Object.entries(grouped).map(([groupKey, skills]) => (
|
|
63
|
+
<div key={groupKey} className="rounded-md border border-border p-3">
|
|
64
|
+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
65
|
+
{copy.groupLabels[groupKey as keyof typeof copy.groupLabels]} ({skills.length})
|
|
66
|
+
</div>
|
|
67
|
+
{skills.length === 0 ? (
|
|
68
|
+
<p className="text-xs text-muted-foreground">{copy.emptyGroup}</p>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="space-y-1.5">
|
|
71
|
+
{skills.map((skill) => (
|
|
72
|
+
<div key={skill.name} className="flex items-center justify-between gap-2">
|
|
73
|
+
<div className="min-w-0">
|
|
74
|
+
<p className="text-sm text-foreground truncate">{skill.name}</p>
|
|
75
|
+
<p className="text-2xs text-muted-foreground">{skill.source === 'builtin' ? copy.sourceBuiltin : copy.sourceUser}</p>
|
|
76
|
+
</div>
|
|
77
|
+
<Toggle size="sm" checked={skill.enabled} onChange={(v) => void mcp.toggleSkill(skill.name, v)} />
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="rounded-md border border-border bg-background p-3">
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => setMatrixOpen((v) => !v)}
|
|
90
|
+
className="w-full flex items-center justify-between text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
91
|
+
>
|
|
92
|
+
<span>{copy.matrixToggle}</span>
|
|
93
|
+
{matrixOpen ? <ChevronDown size={14} className="text-muted-foreground" /> : <ChevronRight size={14} className="text-muted-foreground" />}
|
|
94
|
+
</button>
|
|
95
|
+
{matrixOpen && (
|
|
96
|
+
<div className="mt-3 overflow-x-auto">
|
|
97
|
+
<table className="w-full text-xs">
|
|
98
|
+
<thead>
|
|
99
|
+
<tr className="border-b border-border">
|
|
100
|
+
<th className="text-left py-2 text-muted-foreground font-medium">Skill</th>
|
|
101
|
+
{knownAgents.map((agent) => (
|
|
102
|
+
<th key={agent.key} className="text-left py-2 text-muted-foreground font-medium pr-3">{agent.name}</th>
|
|
103
|
+
))}
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
{filtered.map((skill) => (
|
|
108
|
+
<tr key={skill.name} className="border-b border-border/40">
|
|
109
|
+
<td className="py-2 text-foreground pr-3">{skill.name}</td>
|
|
110
|
+
{knownAgents.map((agent) => (
|
|
111
|
+
<td key={`${skill.name}:${agent.key}`} className="py-2 pr-3 text-muted-foreground">
|
|
112
|
+
{agent.present ? (skill.enabled ? 'Enabled' : 'Disabled') : 'Unsupported'}
|
|
113
|
+
</td>
|
|
114
|
+
))}
|
|
115
|
+
</tr>
|
|
116
|
+
))}
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function SourceFilterButton({
|
|
127
|
+
active,
|
|
128
|
+
label,
|
|
129
|
+
onClick,
|
|
130
|
+
}: {
|
|
131
|
+
active: boolean;
|
|
132
|
+
label: string;
|
|
133
|
+
onClick: () => void;
|
|
134
|
+
}) {
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
className={`px-2.5 h-7 rounded text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
140
|
+
active ? 'bg-[var(--amber-dim)] text-[var(--amber)]' : 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
{label}
|
|
144
|
+
</button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AgentInfo, SkillInfo } from '@/components/settings/types';
|
|
2
|
+
|
|
3
|
+
export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills';
|
|
4
|
+
export type AgentResolvedStatus = 'connected' | 'detected' | 'notFound';
|
|
5
|
+
export type SkillCapability = 'research' | 'coding' | 'docs' | 'ops' | 'memory';
|
|
6
|
+
export type SkillSourceFilter = 'all' | 'builtin' | 'user';
|
|
7
|
+
|
|
8
|
+
export interface RiskItem {
|
|
9
|
+
id: string;
|
|
10
|
+
severity: 'warn' | 'error';
|
|
11
|
+
title: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentBuckets {
|
|
15
|
+
connected: AgentInfo[];
|
|
16
|
+
detected: AgentInfo[];
|
|
17
|
+
notFound: AgentInfo[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseAgentsTab(tab: string | undefined): AgentsDashboardTab {
|
|
21
|
+
if (tab === 'mcp' || tab === 'skills') return tab;
|
|
22
|
+
return 'overview';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function bucketAgents(agents: AgentInfo[]): AgentBuckets {
|
|
26
|
+
return {
|
|
27
|
+
connected: agents.filter((a) => a.present && a.installed),
|
|
28
|
+
detected: agents.filter((a) => a.present && !a.installed),
|
|
29
|
+
notFound: agents.filter((a) => !a.present),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveAgentStatus(agent: AgentInfo): AgentResolvedStatus {
|
|
34
|
+
if (agent.present && agent.installed) return 'connected';
|
|
35
|
+
if (agent.present) return 'detected';
|
|
36
|
+
return 'notFound';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function capabilityForSkill(skill: SkillInfo): SkillCapability {
|
|
40
|
+
const text = `${skill.name} ${skill.description}`.toLowerCase();
|
|
41
|
+
if (text.includes('search') || text.includes('research')) return 'research';
|
|
42
|
+
if (text.includes('doc') || text.includes('write')) return 'docs';
|
|
43
|
+
if (text.includes('deploy') || text.includes('ops') || text.includes('ci')) return 'ops';
|
|
44
|
+
if (text.includes('memory') || text.includes('mind')) return 'memory';
|
|
45
|
+
return 'coding';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function groupSkillsByCapability(skills: SkillInfo[]): Record<SkillCapability, SkillInfo[]> {
|
|
49
|
+
return {
|
|
50
|
+
research: skills.filter((s) => capabilityForSkill(s) === 'research'),
|
|
51
|
+
coding: skills.filter((s) => capabilityForSkill(s) === 'coding'),
|
|
52
|
+
docs: skills.filter((s) => capabilityForSkill(s) === 'docs'),
|
|
53
|
+
ops: skills.filter((s) => capabilityForSkill(s) === 'ops'),
|
|
54
|
+
memory: skills.filter((s) => capabilityForSkill(s) === 'memory'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildRiskQueue(args: {
|
|
59
|
+
mcpRunning: boolean;
|
|
60
|
+
detectedCount: number;
|
|
61
|
+
notFoundCount: number;
|
|
62
|
+
allSkillsDisabled: boolean;
|
|
63
|
+
}): RiskItem[] {
|
|
64
|
+
const items: RiskItem[] = [];
|
|
65
|
+
if (!args.mcpRunning) items.push({ id: 'mcp-stopped', severity: 'error', title: 'MCP server is not running' });
|
|
66
|
+
if (args.detectedCount > 0) items.push({ id: 'detected-unconfigured', severity: 'warn', title: `${args.detectedCount} detected agent(s) need configuration` });
|
|
67
|
+
if (args.notFoundCount > 0) items.push({ id: 'not-found', severity: 'warn', title: `${args.notFoundCount} agent(s) not detected on this machine` });
|
|
68
|
+
if (args.allSkillsDisabled) items.push({ id: 'skills-disabled', severity: 'warn', title: 'All skills are disabled' });
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filterSkills(skills: SkillInfo[], query: string, source: SkillSourceFilter): SkillInfo[] {
|
|
73
|
+
const q = query.trim().toLowerCase();
|
|
74
|
+
return skills.filter((skill) => {
|
|
75
|
+
if (source !== 'all' && skill.source !== source) return false;
|
|
76
|
+
if (!q) return true;
|
|
77
|
+
const haystack = `${skill.name} ${skill.description}`.toLowerCase();
|
|
78
|
+
return haystack.includes(q);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -326,8 +326,14 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
326
326
|
if (!res.ok) {
|
|
327
327
|
let errorMsg = `Request failed (${res.status})`;
|
|
328
328
|
try {
|
|
329
|
-
const errBody = await res.json();
|
|
330
|
-
if (errBody
|
|
329
|
+
const errBody = await res.json() as { error?: { message?: string } | string; message?: string };
|
|
330
|
+
if (typeof errBody?.error === 'string' && errBody.error.trim()) {
|
|
331
|
+
errorMsg = errBody.error;
|
|
332
|
+
} else if (typeof errBody?.error === 'object' && typeof errBody.error?.message === 'string' && errBody.error.message.trim()) {
|
|
333
|
+
errorMsg = errBody.error.message;
|
|
334
|
+
} else if (typeof errBody?.message === 'string' && errBody.message.trim()) {
|
|
335
|
+
errorMsg = errBody.message;
|
|
336
|
+
}
|
|
331
337
|
} catch {}
|
|
332
338
|
throw new Error(errorMsg);
|
|
333
339
|
}
|
|
@@ -600,9 +606,8 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
600
606
|
onChange={e => handleInputChange(e.target.value)}
|
|
601
607
|
onKeyDown={handleInputKeyDown}
|
|
602
608
|
placeholder={t.ask.placeholder}
|
|
603
|
-
disabled={isLoading}
|
|
604
609
|
rows={1}
|
|
605
|
-
className="min-w-0 flex-1 resize-none overflow-y-auto bg-transparent py-2 text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none
|
|
610
|
+
className="min-w-0 flex-1 resize-none overflow-y-auto bg-transparent py-2 text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none"
|
|
606
611
|
/>
|
|
607
612
|
) : (
|
|
608
613
|
<input
|
|
@@ -613,8 +618,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
613
618
|
onChange={e => handleInputChange(e.target.value)}
|
|
614
619
|
onKeyDown={handleInputKeyDown}
|
|
615
620
|
placeholder={t.ask.placeholder}
|
|
616
|
-
|
|
617
|
-
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50 min-w-0"
|
|
621
|
+
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
|
618
622
|
/>
|
|
619
623
|
)}
|
|
620
624
|
|
|
@@ -657,6 +661,11 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
657
661
|
<kbd className="font-mono">ESC</kbd> {t.search.close}
|
|
658
662
|
</span>
|
|
659
663
|
)}
|
|
664
|
+
{isLoading && input.trim() && (
|
|
665
|
+
<span className={isPanel ? 'text-[10px] text-[var(--amber)]/80' : 'text-xs text-[var(--amber)]/80'}>
|
|
666
|
+
{t.ask.draftingHint}
|
|
667
|
+
</span>
|
|
668
|
+
)}
|
|
660
669
|
</div>
|
|
661
670
|
</>
|
|
662
671
|
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { apiFetch } from '@/lib/api';
|
|
6
|
+
|
|
7
|
+
interface ChangeSummaryPayload {
|
|
8
|
+
unreadCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function ChangesBanner() {
|
|
12
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let active = true;
|
|
16
|
+
const fetchSummary = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const summary = await apiFetch<ChangeSummaryPayload>('/api/changes?op=summary');
|
|
19
|
+
if (active) setUnreadCount(summary.unreadCount);
|
|
20
|
+
} catch {
|
|
21
|
+
if (active) setUnreadCount(0);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
void fetchSummary();
|
|
25
|
+
const timer = setInterval(() => void fetchSummary(), 15_000);
|
|
26
|
+
return () => {
|
|
27
|
+
active = false;
|
|
28
|
+
clearInterval(timer);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
if (unreadCount <= 0) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="sticky top-[52px] md:top-0 z-20 border-b bg-[var(--amber-dim)]" style={{ borderColor: 'color-mix(in srgb, var(--amber) 35%, var(--border))' }}>
|
|
36
|
+
<div className="px-4 md:px-6 py-2">
|
|
37
|
+
<div className="content-width xl:mr-[220px] flex items-center justify-between gap-3">
|
|
38
|
+
<p className="text-xs md:text-sm text-foreground font-display">
|
|
39
|
+
{unreadCount} content change{unreadCount === 1 ? '' : 's'} detected.
|
|
40
|
+
</p>
|
|
41
|
+
<Link
|
|
42
|
+
href="/changes"
|
|
43
|
+
className="text-xs md:text-sm text-[var(--amber)] hover:underline focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
44
|
+
>
|
|
45
|
+
Review changes
|
|
46
|
+
</Link>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|