mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 20:44:50 +02:00
[eric] skills: detail page gets claude.ai chrome (byline, enable toggle, kebab, file picker, eye/code); Tools Add menu extracted
This commit is contained in:
@@ -15,10 +15,11 @@ import Collapse from '@mui/material/Collapse';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import ShareIcon from '@mui/icons-material/Share';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
@@ -55,7 +56,8 @@ import {
|
||||
RegistrySkillDetail,
|
||||
} from '@/shared/state/skillRegistrySlice';
|
||||
import { onboardingBus } from '@/app/components/Onboarding/eventBus';
|
||||
import ShareButton from '@/app/components/share/ShareButton';
|
||||
import { requestShare } from '@/app/components/share/ShareRequestHost';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import { IMPORT_OPEN_EVENT } from '@/app/components/share/ImportEntryPoint';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import SkillBuilderChat, { SkillPreviewData } from './SkillBuilderChat';
|
||||
@@ -108,6 +110,7 @@ const Skills: React.FC = () => {
|
||||
const [directoryOpen, setDirectoryOpen] = useState(false);
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [addMenuAnchor, setAddMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [detailMenuAnchor, setDetailMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleBuilderPreview = useCallback((data: SkillPreviewData | null) => {
|
||||
setBuilderPreview(data);
|
||||
@@ -246,29 +249,89 @@ const Skills: React.FC = () => {
|
||||
return selection.type === 'local' && selection.id === key;
|
||||
};
|
||||
|
||||
const ContentPreview: React.FC<{ content: string }> = ({ content }) => (
|
||||
// claude.ai's content card header: [SKILL.md v] file picker + "N files" + eye/code toggles. Single-file skills hide the picker.
|
||||
const ContentPreview: React.FC<{ content: string; skillId?: string; multiFile?: boolean }> = ({ content, skillId, multiFile }) => {
|
||||
const [files, setFiles] = useState<{ path: string; content: string }[]>([]);
|
||||
const [selectedPath, setSelectedPath] = useState('SKILL.md');
|
||||
const [fileMenuAnchor, setFileMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
useEffect(() => {
|
||||
setFiles([]);
|
||||
setSelectedPath('SKILL.md');
|
||||
if (!skillId || !multiFile) return;
|
||||
let stale = false;
|
||||
void fetch(`${API_BASE}/skills/${skillId}/files`)
|
||||
.then((res) => (res.ok ? res.json() : { files: [] }))
|
||||
.then((data: { files?: { path: string; content: string }[] }) => { if (!stale) setFiles(data.files ?? []); })
|
||||
.catch(() => { /* picker quietly falls back to SKILL.md */ });
|
||||
return () => { stale = true; };
|
||||
}, [skillId, multiFile]);
|
||||
const selected = files.find((f) => f.path === selectedPath);
|
||||
const shownContent = selected ? selected.content : content;
|
||||
const isMarkdown = selectedPath.toLowerCase().endsWith('.md');
|
||||
const view = isMarkdown ? contentView : 'raw';
|
||||
const iconBtnSx = (active: boolean) => ({
|
||||
color: active ? c.text.primary : c.text.tertiary,
|
||||
bgcolor: active ? c.bg.secondary : 'transparent',
|
||||
borderRadius: `${c.radius.sm}px`,
|
||||
'&:hover': { color: c.text.primary },
|
||||
});
|
||||
return (
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1, flexShrink: 0 }}>
|
||||
<ToggleButtonGroup
|
||||
value={contentView}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v) setContentView(v); }}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiToggleButton-root': {
|
||||
color: c.text.tertiary, border: `1px solid ${c.border.medium}`,
|
||||
textTransform: 'none', fontSize: '0.75rem', py: 0.25, px: 1.2, lineHeight: 1.4,
|
||||
'&.Mui-selected': { bgcolor: c.bg.secondary, color: c.text.primary, borderColor: c.border.strong },
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.03)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="preview"><VisibilityIcon sx={{ fontSize: 14, mr: 0.5 }} />Preview</ToggleButton>
|
||||
<ToggleButton value="raw"><CodeIcon sx={{ fontSize: 14, mr: 0.5 }} />Raw</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1, flexShrink: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{files.length > 1 ? (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 15 }} />}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => setFileMenuAnchor(e.currentTarget)}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.75rem', fontWeight: 600, px: 1.25, py: 0.3,
|
||||
color: c.text.primary, bgcolor: c.bg.secondary, borderRadius: `${c.radius.md}px`,
|
||||
fontFamily: c.font.mono, '&:hover': { bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
{selectedPath}
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={fileMenuAnchor}
|
||||
open={!!fileMenuAnchor}
|
||||
onClose={() => setFileMenuAnchor(null)}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 200 } }}
|
||||
>
|
||||
{files.map((f) => (
|
||||
<MenuItem
|
||||
key={f.path}
|
||||
onClick={() => { setSelectedPath(f.path); setFileMenuAnchor(null); }}
|
||||
sx={{ fontSize: '0.8125rem', fontFamily: c.font.mono, color: f.path === selectedPath ? c.text.primary : c.text.secondary, '&:hover': { bgcolor: c.bg.secondary } }}
|
||||
>
|
||||
{f.path}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
<Typography sx={{ fontSize: '0.75rem', color: c.text.ghost }}>
|
||||
{files.length} files
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.25 }}>
|
||||
<Tooltip title="Preview">
|
||||
<span>
|
||||
<IconButton size="small" disabled={!isMarkdown} onClick={() => setContentView('preview')} sx={iconBtnSx(view === 'preview')}>
|
||||
<VisibilityIcon sx={{ fontSize: 15 }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Raw">
|
||||
<IconButton size="small" onClick={() => setContentView('raw')} sx={iconBtnSx(view === 'raw')}>
|
||||
<CodeIcon sx={{ fontSize: 15 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{contentView === 'raw' ? (
|
||||
{view === 'raw' ? (
|
||||
<Box sx={{
|
||||
flex: 1, minHeight: 0,
|
||||
bgcolor: c.bg.secondary,
|
||||
@@ -281,7 +344,7 @@ const Skills: React.FC = () => {
|
||||
color: c.text.secondary, fontSize: '0.8125rem', fontFamily: c.font.mono,
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-word', m: 0, lineHeight: 1.65,
|
||||
}}>
|
||||
{content}
|
||||
{shownContent}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -305,11 +368,12 @@ const Skills: React.FC = () => {
|
||||
'& a': { color: c.accent.primary, textDecoration: 'none', '&:hover': { textDecoration: 'underline' } },
|
||||
'& strong': { fontWeight: 600, color: c.text.primary },
|
||||
}}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{shownContent}</ReactMarkdown>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarRow: React.FC<{
|
||||
label: string;
|
||||
@@ -656,79 +720,71 @@ const Skills: React.FC = () => {
|
||||
) : null
|
||||
) : selectedLocal ? (
|
||||
<Box sx={{ p: 4, pb: 3, maxWidth: 1100, display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5, flexShrink: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ fontSize: '1.375rem', fontWeight: 700, color: c.text.primary, fontFamily: c.font.sans }}>
|
||||
{selectedLocal.name}
|
||||
{/* claude.ai detail chrome: title + info, byline underneath, enable toggle + kebab on the right. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1, flexShrink: 0 }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Typography sx={{ fontSize: '1.375rem', fontWeight: 700, color: c.text.primary, fontFamily: c.font.sans }}>
|
||||
{selectedLocal.name}
|
||||
</Typography>
|
||||
{selectedLocal.command && (
|
||||
<Tooltip title={`Slash command: /${selectedLocal.command}`}>
|
||||
<InfoOutlinedIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{regOutdated.includes(selectedLocal.id) && (
|
||||
<Chip
|
||||
label="Update available"
|
||||
size="small"
|
||||
sx={{ bgcolor: `${c.status.warning}22`, color: c.status.warning, fontWeight: 600, fontSize: '0.6875rem', height: 20 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.75rem', color: c.text.ghost, mt: 0.25 }}>
|
||||
by {selectedLocal.built_in ? 'OpenSwarm' : /anthropic/i.test(selectedLocal.source || '') ? 'Anthropic' : selectedLocal.source ? selectedLocal.source.split('/')[0] : 'You'}
|
||||
</Typography>
|
||||
{selectedLocal.built_in && (
|
||||
<Chip
|
||||
label="Built-in"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(174,86,48,0.12)',
|
||||
color: c.accent.primary,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.6875rem',
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{regOutdated.includes(selectedLocal.id) && (
|
||||
<Chip
|
||||
label="Update available"
|
||||
size="small"
|
||||
sx={{ bgcolor: `${c.status.warning}22`, color: c.status.warning, fontWeight: 600, fontSize: '0.6875rem', height: 20 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
|
||||
{regOutdated.includes(selectedLocal.id) && (
|
||||
<Button
|
||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', flexShrink: 0 }}>
|
||||
<Tooltip title={selectedLocal.enabled === false ? 'Enable skill' : 'Disable skill'}>
|
||||
<Switch
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<DownloadIcon sx={{ fontSize: 16 }} />}
|
||||
disabled={updatingId === selectedLocal.id}
|
||||
onClick={() => handleUpdate(selectedLocal)}
|
||||
sx={{ textTransform: 'none', fontSize: '0.75rem', py: 0.3, bgcolor: c.status.warning, '&:hover': { bgcolor: c.status.warning } }}
|
||||
>
|
||||
{updatingId === selectedLocal.id ? 'Updating...' : 'Update'}
|
||||
</Button>
|
||||
)}
|
||||
<ShareButton target={{ kind: 'skill', id: selectedLocal.id, name: selectedLocal.name }} />
|
||||
<Tooltip title="Edit">
|
||||
<IconButton size="small" onClick={() => openEdit(selectedLocal)} sx={{ color: c.text.tertiary, '&:hover': { color: c.accent.primary } }}>
|
||||
<EditIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
checked={selectedLocal.enabled !== false}
|
||||
onChange={() => { void dispatch(updateSkill({ id: selectedLocal.id, enabled: selectedLocal.enabled === false })); }}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!selectedLocal.built_in && (
|
||||
<Tooltip title="Delete">
|
||||
<IconButton size="small" onClick={() => handleDelete(selectedLocal.id)} sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error } }}>
|
||||
<DeleteIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton size="small" onClick={(e: React.MouseEvent<HTMLElement>) => setDetailMenuAnchor(e.currentTarget)} sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
|
||||
<MoreHorizIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={detailMenuAnchor}
|
||||
open={!!detailMenuAnchor}
|
||||
onClose={() => setDetailMenuAnchor(null)}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 180 } }}
|
||||
>
|
||||
<MenuItem onClick={() => { setDetailMenuAnchor(null); openEdit(selectedLocal); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<EditIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { setDetailMenuAnchor(null); requestShare({ kind: 'skill', id: selectedLocal.id, name: selectedLocal.name }); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<ShareIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Share as .swarm…
|
||||
</MenuItem>
|
||||
{regOutdated.includes(selectedLocal.id) && (
|
||||
<MenuItem disabled={updatingId === selectedLocal.id} onClick={() => { setDetailMenuAnchor(null); void handleUpdate(selectedLocal); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<DownloadIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
{updatingId === selectedLocal.id ? 'Updating…' : 'Update to latest'}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!selectedLocal.built_in && (
|
||||
<MenuItem onClick={() => { setDetailMenuAnchor(null); void handleDelete(selectedLocal.id); }} sx={{ fontSize: '0.875rem', color: c.status.error, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<DeleteIcon sx={{ fontSize: 16, color: c.status.error }} />
|
||||
Delete
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{selectedLocal.command && (
|
||||
<Box sx={{ mb: 1.5, flexShrink: 0 }}>
|
||||
<Chip
|
||||
icon={<TerminalIcon sx={{ fontSize: 14 }} />}
|
||||
label={`/${selectedLocal.command}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(174,86,48,0.08)', color: c.accent.primary,
|
||||
fontWeight: 500, fontSize: '0.75rem', height: 26,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 1, flexShrink: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.75rem', color: c.text.ghost }}>Added by <strong style={{ color: c.text.secondary, fontWeight: 600 }}>You</strong></Typography>
|
||||
</Box>
|
||||
|
||||
{selectedLocal.description && (
|
||||
<Box sx={{ mb: 2, flexShrink: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.875rem', color: c.text.secondary, lineHeight: 1.6 }}>
|
||||
@@ -737,7 +793,7 @@ const Skills: React.FC = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ContentPreview content={selectedLocal.content} />
|
||||
<ContentPreview content={selectedLocal.content} skillId={selectedLocal.id} multiFile={selectedLocal.has_supporting_files} />
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
@@ -2,19 +2,15 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import StorefrontIcon from '@mui/icons-material/Storefront';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
fetchTools,
|
||||
@@ -35,10 +31,8 @@ import ToolSection from './cards/ToolSection';
|
||||
import BrowserPermissionCard from './cards/BrowserPermissionCard';
|
||||
import AgentWorkflowsSection from './cards/AgentWorkflowsSection';
|
||||
import RegistryBrowserDialog from './dialogs/RegistryBrowserDialog';
|
||||
import ToolsAddMenu from './dialogs/ToolsAddMenu';
|
||||
import ToolDialogs from './dialogs/ToolDialogs';
|
||||
import DirectoryDialog from '../Directory/DirectoryDialog';
|
||||
import AddCustomConnectorDialog from '../Directory/AddCustomConnectorDialog';
|
||||
import AddLinkIcon from '@mui/icons-material/AddLink';
|
||||
import CustomToolCard from './cards/CustomToolCard';
|
||||
import IntegrationGalleryCard from './cards/IntegrationGalleryCard';
|
||||
import { useToolsActions } from './hooks/useToolsActions';
|
||||
@@ -72,9 +66,6 @@ const Tools: React.FC = () => {
|
||||
const [browserSectionOpen, setBrowserSectionOpen] = useState(false);
|
||||
const [browserCollapsed, setBrowserCollapsed] = useState<Record<string, boolean>>({ browser_delegation: true, browser_action: true });
|
||||
const [builtinSectionOpen, setBuiltinSectionOpen] = useState(true);
|
||||
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [directoryOpen, setDirectoryOpen] = useState(false);
|
||||
const [customConnectorOpen, setCustomConnectorOpen] = useState(false);
|
||||
// claude.ai's Connectors page tabs: All / Connected / Not connected.
|
||||
const [connFilter, setConnFilter] = useState<'all' | 'connected' | 'not-connected'>('all');
|
||||
const visibleTools = useMemo(() => {
|
||||
@@ -82,10 +73,7 @@ const Tools: React.FC = () => {
|
||||
if (connFilter === 'not-connected') return tools.filter((t) => t.enabled === false);
|
||||
return tools;
|
||||
}, [tools, connFilter]);
|
||||
const visibleGallery = useMemo(
|
||||
() => (connFilter === 'connected' ? [] : uninstalledIntegrations),
|
||||
[uninstalledIntegrations, connFilter],
|
||||
);
|
||||
const visibleGallery = useMemo(() => (connFilter === 'connected' ? [] : uninstalledIntegrations), [uninstalledIntegrations, connFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchTools());
|
||||
@@ -100,11 +88,8 @@ const Tools: React.FC = () => {
|
||||
|
||||
const toggleCategory = (cat: string) => setCollapsedCategories((p) => ({ ...p, [cat]: !p[cat] }));
|
||||
const toggleBuiltinExpand = (name: string) => setExpandedBuiltin((p) => (p === name ? null : name));
|
||||
|
||||
const handleMenuOpen = (e: React.MouseEvent<HTMLElement>) => setMenuAnchor(e.currentTarget);
|
||||
const handleMenuClose = () => setMenuAnchor(null);
|
||||
|
||||
const a = useToolsActions({ items, allTools, regServersRaw, closeMenu: handleMenuClose });
|
||||
// The Add menu closes itself now (ToolsAddMenu), so the hook's closeMenu hook-in is a no-op.
|
||||
const a = useToolsActions({ items, allTools, regServersRaw, closeMenu: () => {} });
|
||||
|
||||
// Curated whitelist matches the MCPSearch alias map in main.py (mcp-meta).
|
||||
const CURATED_MCP_NAMES = useMemo(() => new Set([
|
||||
@@ -123,43 +108,13 @@ const Tools: React.FC = () => {
|
||||
<Box sx={{ px: 3, pt: 1, pb: 3, height: '100%', overflow: 'auto' }}>
|
||||
{/* The pane header already says "Tools"; a slim action row beats a second page title. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', mb: 1.5 }}>
|
||||
<Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<AddIcon sx={{ fontSize: 16 }} />}
|
||||
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={handleMenuOpen}
|
||||
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.pressed }, textTransform: 'none', borderRadius: 2, fontSize: '0.8125rem' }}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={menuAnchor}
|
||||
open={!!menuAnchor}
|
||||
onClose={handleMenuClose}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: 2, mt: 0.5, minWidth: 200 } }}
|
||||
>
|
||||
<MenuItem onClick={() => { handleMenuClose(); setDirectoryOpen(true); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<StorefrontIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Browse connectors
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { handleMenuClose(); setCustomConnectorOpen(true); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<AddLinkIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Add custom connector
|
||||
</MenuItem>
|
||||
<MenuItem onClick={a.openCreate} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<BuildIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Create Custom
|
||||
</MenuItem>
|
||||
{devMode && (
|
||||
<MenuItem onClick={a.openRegistryBrowser} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<StorefrontIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Browse MCP Registry
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
<ToolsAddMenu
|
||||
devMode={!!devMode}
|
||||
onOpenCreate={a.openCreate}
|
||||
onOpenRegistry={a.openRegistryBrowser}
|
||||
onOpenInstalledConnector={(toolId) => a.setExpandedToolId(toolId)}
|
||||
onSnackbar={(message, severity) => a.setSnackbar({ open: true, message, severity: severity === 'error' ? 'error' : undefined })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
@@ -296,22 +251,6 @@ const Tools: React.FC = () => {
|
||||
onCredentialsSave={a.handleCredentialsSave}
|
||||
/>
|
||||
|
||||
<DirectoryDialog
|
||||
open={directoryOpen}
|
||||
initialTab="connectors"
|
||||
onClose={() => setDirectoryOpen(false)}
|
||||
onOpenInstalledConnector={(toolId) => {
|
||||
setDirectoryOpen(false);
|
||||
a.setExpandedToolId(toolId);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AddCustomConnectorDialog
|
||||
open={customConnectorOpen}
|
||||
onClose={() => setCustomConnectorOpen(false)}
|
||||
onBrowsePrebuilt={() => setDirectoryOpen(true)}
|
||||
onAdded={(message, severity) => a.setSnackbar({ open: true, message, severity: severity === 'error' ? 'error' : undefined })}
|
||||
/>
|
||||
|
||||
<RegistryBrowserDialog
|
||||
open={a.registryOpen}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import StorefrontIcon from '@mui/icons-material/Storefront';
|
||||
import AddLinkIcon from '@mui/icons-material/AddLink';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import DirectoryDialog from '../../Directory/DirectoryDialog';
|
||||
import AddCustomConnectorDialog from '../../Directory/AddCustomConnectorDialog';
|
||||
|
||||
interface Props {
|
||||
devMode: boolean;
|
||||
onOpenCreate: () => void;
|
||||
onOpenRegistry: () => void;
|
||||
onOpenInstalledConnector: (toolId: string) => void;
|
||||
onSnackbar: (message: string, severity: 'success' | 'error') => void;
|
||||
}
|
||||
|
||||
// The Tools page's Add button (claude.ai's Connectors-page grammar) plus the Directory and
|
||||
// custom-connector dialogs it opens.
|
||||
const ToolsAddMenu: React.FC<Props> = ({ devMode, onOpenCreate, onOpenRegistry, onOpenInstalledConnector, onSnackbar }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [directoryOpen, setDirectoryOpen] = useState(false);
|
||||
const [customConnectorOpen, setCustomConnectorOpen] = useState(false);
|
||||
const closeMenu = () => setMenuAnchor(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<AddIcon sx={{ fontSize: 16 }} />}
|
||||
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => setMenuAnchor(e.currentTarget)}
|
||||
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.pressed }, textTransform: 'none', borderRadius: 2, fontSize: '0.8125rem' }}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={menuAnchor}
|
||||
open={!!menuAnchor}
|
||||
onClose={closeMenu}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: 2, mt: 0.5, minWidth: 200 } }}
|
||||
>
|
||||
<MenuItem onClick={() => { closeMenu(); setDirectoryOpen(true); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<StorefrontIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Browse connectors
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { closeMenu(); setCustomConnectorOpen(true); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<AddLinkIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Add custom connector
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { closeMenu(); onOpenCreate(); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<BuildIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Create Custom
|
||||
</MenuItem>
|
||||
{devMode && (
|
||||
<MenuItem onClick={() => { closeMenu(); onOpenRegistry(); }} sx={{ color: c.text.primary, fontSize: '0.875rem', gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<StorefrontIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Browse MCP Registry
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<DirectoryDialog
|
||||
open={directoryOpen}
|
||||
initialTab="connectors"
|
||||
onClose={() => setDirectoryOpen(false)}
|
||||
onOpenInstalledConnector={(toolId) => {
|
||||
setDirectoryOpen(false);
|
||||
onOpenInstalledConnector(toolId);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AddCustomConnectorDialog
|
||||
open={customConnectorOpen}
|
||||
onClose={() => setCustomConnectorOpen(false)}
|
||||
onBrowsePrebuilt={() => setDirectoryOpen(true)}
|
||||
onAdded={onSnackbar}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsAddMenu;
|
||||
@@ -16,6 +16,10 @@ export interface Skill {
|
||||
source?: string;
|
||||
folder?: string;
|
||||
version?: string;
|
||||
/** Folder skill with files beyond SKILL.md; the detail page offers a file picker for those. */
|
||||
has_supporting_files?: boolean;
|
||||
/** Detail-page toggle: disabled skills stay installed but leave the agent's skill list. */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface SkillsState {
|
||||
|
||||
Reference in New Issue
Block a user