@nospt/plugin-dev-ai-hub 0.1.0

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.
Files changed (55) hide show
  1. package/.eslintrc.js +1 -0
  2. package/dev/index.tsx +232 -0
  3. package/dist/api/DevAiHubClient.esm.js +76 -0
  4. package/dist/api/DevAiHubClient.esm.js.map +1 -0
  5. package/dist/components/AssetCard/AssetCard.esm.js +194 -0
  6. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -0
  7. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +324 -0
  8. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -0
  9. package/dist/components/AssetFilters/AssetFilters.esm.js +216 -0
  10. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -0
  11. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +194 -0
  12. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -0
  13. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +320 -0
  14. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -0
  15. package/dist/components/DevAiHubPage/index.esm.js +6 -0
  16. package/dist/components/DevAiHubPage/index.esm.js.map +1 -0
  17. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +274 -0
  18. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +1 -0
  19. package/dist/components/ToolIcon/ToolIcon.esm.js +41 -0
  20. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -0
  21. package/dist/hooks/index.esm.js +114 -0
  22. package/dist/hooks/index.esm.js.map +1 -0
  23. package/dist/index.d.ts +104 -0
  24. package/dist/index.esm.js +5 -0
  25. package/dist/index.esm.js.map +1 -0
  26. package/dist/plugin.esm.js +40 -0
  27. package/dist/plugin.esm.js.map +1 -0
  28. package/dist/pluginLegacy.esm.js +27 -0
  29. package/dist/pluginLegacy.esm.js.map +1 -0
  30. package/dist/routes.esm.js +6 -0
  31. package/dist/routes.esm.js.map +1 -0
  32. package/package.json +80 -0
  33. package/package.json-prepack +75 -0
  34. package/src/api/DevAiHubClient.ts +110 -0
  35. package/src/components/AssetCard/AssetCard.test.tsx +182 -0
  36. package/src/components/AssetCard/AssetCard.tsx +221 -0
  37. package/src/components/AssetCard/index.ts +1 -0
  38. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +377 -0
  39. package/src/components/AssetDetailPanel/index.ts +1 -0
  40. package/src/components/AssetFilters/AssetFilters.tsx +261 -0
  41. package/src/components/AssetFilters/index.ts +2 -0
  42. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +216 -0
  43. package/src/components/AssetInstallDialog/index.ts +1 -0
  44. package/src/components/DevAiHubPage/DevAiHubPage.tsx +346 -0
  45. package/src/components/DevAiHubPage/index.ts +1 -0
  46. package/src/components/McpConfigDialog/McpConfigDialog.tsx +301 -0
  47. package/src/components/McpConfigDialog/index.ts +1 -0
  48. package/src/components/ToolIcon/ToolIcon.tsx +49 -0
  49. package/src/components/ToolIcon/index.ts +1 -0
  50. package/src/hooks/index.ts +152 -0
  51. package/src/index.ts +10 -0
  52. package/src/plugin.tsx +47 -0
  53. package/src/pluginLegacy.ts +42 -0
  54. package/src/routes.ts +3 -0
  55. package/tsconfig.json +10 -0
@@ -0,0 +1,346 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ import Box from '@mui/material/Box';
4
+ import Button from '@mui/material/Button';
5
+ import Grid from '@mui/material/Grid';
6
+ import Pagination from '@mui/material/Pagination';
7
+ import Skeleton from '@mui/material/Skeleton';
8
+ import Tooltip from '@mui/material/Tooltip';
9
+ import Typography from '@mui/material/Typography';
10
+ import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
11
+ import ArticleIcon from '@mui/icons-material/Article';
12
+ import SmartToyIcon from '@mui/icons-material/SmartToy';
13
+ import BuildIcon from '@mui/icons-material/Build';
14
+ import AccountTreeIcon from '@mui/icons-material/AccountTree';
15
+ import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
16
+ import HubIcon from '@mui/icons-material/Hub';
17
+ import { Content, Header, Page } from '@backstage/core-components';
18
+ import type { AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
19
+ import { AssetCard } from '../AssetCard';
20
+ import { AssetFilters } from '../AssetFilters';
21
+ import type { AssetFiltersValue } from '../AssetFilters';
22
+ import { AssetDetailPanel } from '../AssetDetailPanel';
23
+ import { AssetInstallDialog } from '../AssetInstallDialog';
24
+ import { McpConfigDialog } from '../McpConfigDialog';
25
+ import { ToolIcon } from '../ToolIcon';
26
+ import { useAssets, useStats, useProviders } from '../../hooks';
27
+
28
+ const SUPPORTED_TOOLS: AiTool[] = ['claude-code', 'github-copilot', 'google-gemini', 'cursor'];
29
+
30
+ const TOOL_LABELS: Record<AiTool, string> = {
31
+ 'all': 'Universal',
32
+ 'claude-code': 'Claude Code',
33
+ 'github-copilot': 'GitHub Copilot',
34
+ 'google-gemini': 'Google Gemini',
35
+ 'cursor': 'Cursor',
36
+ };
37
+
38
+ function timeAgo(iso: string): string {
39
+ const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
40
+ if (diff < 60) return 'just now';
41
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
42
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
43
+ return `${Math.floor(diff / 86400)}d ago`;
44
+ }
45
+
46
+ const DEFAULT_FILTERS: AssetFiltersValue = {
47
+ types: [],
48
+ tools: [],
49
+ search: '',
50
+ tags: [],
51
+ providerId: undefined,
52
+ };
53
+
54
+ const PAGE_SIZE = 24;
55
+
56
+ const STATS_CONFIG = [
57
+ { key: 'instruction' as AssetType, label: 'Instructions', Icon: ArticleIcon,
58
+ gradient: 'linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%)', shadow: '#2563EB40' },
59
+ { key: 'agent' as AssetType, label: 'Agents', Icon: SmartToyIcon,
60
+ gradient: 'linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)', shadow: '#7C3AED40' },
61
+ { key: 'skill' as AssetType, label: 'Skills', Icon: BuildIcon,
62
+ gradient: 'linear-gradient(135deg, #059669 0%, #047857 100%)', shadow: '#05966940' },
63
+ { key: 'workflow' as AssetType, label: 'Workflows', Icon: AccountTreeIcon,
64
+ gradient: 'linear-gradient(135deg, #D97706 0%, #B45309 100%)', shadow: '#D9770640' },
65
+ ];
66
+
67
+ export function DevAiHubPage() {
68
+ const [filters, setFilters] = useState<AssetFiltersValue>(DEFAULT_FILTERS);
69
+ const [page, setPage] = useState(1);
70
+ const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
71
+
72
+ const [searchParams, setSearchParams] = useSearchParams();
73
+ const selectedAssetId = searchParams.get('assetId');
74
+ const installAssetId = searchParams.get('installId');
75
+
76
+ const handleViewAsset = (id: string) =>
77
+ setSearchParams(p => { const n = new URLSearchParams(p); n.set('assetId', id); return n; });
78
+
79
+ const handleCloseDetail = () =>
80
+ setSearchParams(p => { const n = new URLSearchParams(p); n.delete('assetId'); return n; });
81
+
82
+ const handleInstallAsset = (id: string) =>
83
+ setSearchParams(p => { const n = new URLSearchParams(p); n.set('installId', id); return n; });
84
+
85
+ const handleCloseInstall = () =>
86
+ setSearchParams(p => { const n = new URLSearchParams(p); n.delete('installId'); return n; });
87
+
88
+ const { stats } = useStats();
89
+ const { providers } = useProviders();
90
+
91
+ const apiFilter = useMemo(
92
+ () => ({
93
+ type: filters.types.length === 1 ? (filters.types[0] as AssetType) : undefined,
94
+ tool: filters.tools.length === 1 ? (filters.tools[0] as AiTool) : undefined,
95
+ search: filters.search || undefined,
96
+ tags: filters.tags.length > 0 ? filters.tags : undefined,
97
+ providerId: filters.providerId || undefined,
98
+ page,
99
+ pageSize: PAGE_SIZE,
100
+ }),
101
+ [filters, page],
102
+ );
103
+
104
+ const { result, loading } = useAssets(apiFilter);
105
+
106
+ const handleFiltersChange = (next: AssetFiltersValue) => {
107
+ setFilters(next);
108
+ setPage(1);
109
+ };
110
+
111
+ const totalPages = result ? Math.ceil(result.totalCount / PAGE_SIZE) : 0;
112
+
113
+ const availableTags = useMemo(() => {
114
+ if (!result) return [];
115
+ const tagSet = new Set<string>();
116
+ result.items.forEach(a => a.tags.forEach(t => tagSet.add(t)));
117
+ return Array.from(tagSet).sort();
118
+ }, [result]);
119
+
120
+ return (
121
+ <Page themeId="tool">
122
+ <Header
123
+ title={
124
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
125
+ <Box
126
+ sx={{
127
+ display: 'flex',
128
+ alignItems: 'center',
129
+ justifyContent: 'center',
130
+ width: 44,
131
+ height: 44,
132
+ borderRadius: 2.5,
133
+ background: 'rgba(255,255,255,0.15)',
134
+ backdropFilter: 'blur(8px)',
135
+ border: '1px solid rgba(255,255,255,0.3)',
136
+ boxShadow: '0 2px 12px rgba(0,0,0,0.15)',
137
+ flexShrink: 0,
138
+ }}
139
+ >
140
+ <HubIcon sx={{ fontSize: '1.5rem', color: '#fff' }} />
141
+ </Box>
142
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3 }}>
143
+ <Box sx={{ fontSize: '1.75rem', fontWeight: 700, lineHeight: 1, color: '#fff' }}>
144
+ Dev AI Hub
145
+ </Box>
146
+ <Box sx={{ fontSize: '0.85rem', fontWeight: 400, color: 'rgba(255,255,255,0.75)', lineHeight: 1 }}>
147
+ {stats
148
+ ? `${stats.totalAssets} assets · ${Object.keys(stats.byProvider).length} provider${Object.keys(stats.byProvider).length !== 1 ? 's' : ''}`
149
+ : 'Centralized AI assets for your team'}
150
+ </Box>
151
+ </Box>
152
+ </Box>
153
+ }
154
+ pageTitleOverride="Dev AI Hub"
155
+ >
156
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
157
+ {/* Supported tools */}
158
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
159
+ {SUPPORTED_TOOLS.map(tool => (
160
+ <Tooltip key={tool} title={TOOL_LABELS[tool]} arrow>
161
+ <Box
162
+ sx={{
163
+ width: 32,
164
+ height: 32,
165
+ borderRadius: '50%',
166
+ backgroundColor: 'rgba(255,255,255,0.15)',
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ justifyContent: 'center',
170
+ backdropFilter: 'blur(4px)',
171
+ transition: 'background-color 0.15s',
172
+ '&:hover': { backgroundColor: 'rgba(255,255,255,0.25)' },
173
+ }}
174
+ >
175
+ <ToolIcon tool={tool} branded={false} sx={{ fontSize: '1rem', color: '#fff' }} />
176
+ </Box>
177
+ </Tooltip>
178
+ ))}
179
+ </Box>
180
+
181
+ {/* Last sync status */}
182
+ {stats?.lastSync && (
183
+ <Tooltip title={`Last sync: ${new Date(stats.lastSync).toLocaleString()}`} arrow>
184
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'default' }}>
185
+ <FiberManualRecordIcon sx={{ fontSize: '0.6rem', color: '#4ade80' }} />
186
+ <Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.8)', whiteSpace: 'nowrap' }}>
187
+ {timeAgo(stats.lastSync)}
188
+ </Typography>
189
+ </Box>
190
+ </Tooltip>
191
+ )}
192
+
193
+ {/* Configure MCP button */}
194
+ <Button
195
+ variant="contained"
196
+ size="small"
197
+ startIcon={<SettingsEthernetIcon />}
198
+ onClick={() => setMcpDialogOpen(true)}
199
+ sx={{
200
+ borderRadius: 2,
201
+ fontWeight: 700,
202
+ whiteSpace: 'nowrap',
203
+ background: 'rgba(255,255,255,0.15)',
204
+ backdropFilter: 'blur(4px)',
205
+ border: '1px solid rgba(255,255,255,0.3)',
206
+ color: '#fff',
207
+ boxShadow: 'none',
208
+ '&:hover': {
209
+ background: 'rgba(255,255,255,0.25)',
210
+ boxShadow: 'none',
211
+ },
212
+ }}
213
+ >
214
+ Configure MCP
215
+ </Button>
216
+ </Box>
217
+ </Header>
218
+
219
+ <Content>
220
+
221
+ {/* Stats row */}
222
+ <Grid container spacing={2} sx={{ mb: 3 }}>
223
+ {STATS_CONFIG.map(({ key, label, Icon, gradient, shadow }) => (
224
+ <Grid item xs={6} sm={3} key={key}>
225
+ <Box
226
+ onClick={() => handleFiltersChange({ ...filters, types: filters.types[0] === key ? [] : [key] })}
227
+ sx={{
228
+ background: gradient,
229
+ borderRadius: 3,
230
+ p: 2,
231
+ cursor: 'pointer',
232
+ boxShadow: filters.types[0] === key ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
233
+ transform: filters.types[0] === key ? 'translateY(-2px)' : 'none',
234
+ transition: 'all 0.2s ease',
235
+ outline: filters.types[0] === key ? '2px solid rgba(255,255,255,0.6)' : 'none',
236
+ outlineOffset: 2,
237
+ '&:hover': {
238
+ boxShadow: `0 8px 24px ${shadow}`,
239
+ transform: 'translateY(-2px)',
240
+ },
241
+ }}
242
+ >
243
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
244
+ <Box>
245
+ <Typography variant="h4" fontWeight={800} sx={{ color: '#fff', lineHeight: 1 }}>
246
+ {stats ? (stats.byType[key] ?? 0) : <Skeleton width={32} sx={{ bgcolor: 'rgba(255,255,255,0.3)' }} />}
247
+ </Typography>
248
+ <Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)', fontWeight: 500, mt: 0.5 }}>
249
+ {label}
250
+ </Typography>
251
+ </Box>
252
+ <Box
253
+ sx={{
254
+ width: 40,
255
+ height: 40,
256
+ borderRadius: 2,
257
+ backgroundColor: 'rgba(255,255,255,0.2)',
258
+ display: 'flex',
259
+ alignItems: 'center',
260
+ justifyContent: 'center',
261
+ }}
262
+ >
263
+ <Icon sx={{ color: '#fff', fontSize: '1.4rem' }} />
264
+ </Box>
265
+ </Box>
266
+ </Box>
267
+ </Grid>
268
+ ))}
269
+ </Grid>
270
+
271
+ {/* Filters */}
272
+ <AssetFilters
273
+ value={filters}
274
+ onChange={handleFiltersChange}
275
+ availableTags={availableTags}
276
+ providers={providers.length > 1 ? providers : undefined}
277
+ />
278
+
279
+ {/* Results summary */}
280
+ {result && !loading && (
281
+ <Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
282
+ {result.totalCount} asset{result.totalCount !== 1 ? 's' : ''} found
283
+ </Typography>
284
+ )}
285
+
286
+ {/* Asset grid — 4 columns on large screens */}
287
+ <Grid container spacing={1.5}>
288
+ {loading
289
+ ? Array.from({ length: 8 }).map((_, i) => (
290
+ <Grid item xs={12} sm={6} md={4} lg={3} key={i}>
291
+ <Skeleton variant="rectangular" height={150} sx={{ borderRadius: 2 }} />
292
+ </Grid>
293
+ ))
294
+ : result?.items.map(asset => (
295
+ <Grid item xs={12} sm={6} md={4} lg={3} key={asset.id}>
296
+ <AssetCard
297
+ asset={asset}
298
+ onView={handleViewAsset}
299
+ onInstall={handleInstallAsset}
300
+ />
301
+ </Grid>
302
+ ))}
303
+ </Grid>
304
+
305
+ {/* Empty state */}
306
+ {!loading && result?.items.length === 0 && (
307
+ <Box sx={{ py: 12, textAlign: 'center' }}>
308
+ <Typography variant="h1" sx={{ mb: 2, opacity: 0.15, fontSize: '5rem' }}>🤖</Typography>
309
+ <Typography variant="h6" color="text.secondary" fontWeight={600}>No assets found</Typography>
310
+ <Typography variant="body2" color="text.disabled" sx={{ mt: 0.5 }}>
311
+ Try adjusting your filters or search terms.
312
+ </Typography>
313
+ </Box>
314
+ )}
315
+
316
+ {/* Pagination */}
317
+ {totalPages > 1 && (
318
+ <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
319
+ <Pagination
320
+ count={totalPages}
321
+ page={page}
322
+ onChange={(_, p) => setPage(p)}
323
+ color="primary"
324
+ shape="rounded"
325
+ />
326
+ </Box>
327
+ )}
328
+ </Content>
329
+
330
+ <AssetDetailPanel
331
+ assetId={selectedAssetId}
332
+ onClose={handleCloseDetail}
333
+ />
334
+
335
+ <AssetInstallDialog
336
+ assetId={installAssetId}
337
+ onClose={handleCloseInstall}
338
+ />
339
+
340
+ <McpConfigDialog
341
+ open={mcpDialogOpen}
342
+ onClose={() => setMcpDialogOpen(false)}
343
+ />
344
+ </Page>
345
+ );
346
+ }
@@ -0,0 +1 @@
1
+ export { DevAiHubPage } from './DevAiHubPage';
@@ -0,0 +1,301 @@
1
+ import { useEffect, useState } from 'react';
2
+ import Box from '@mui/material/Box';
3
+ import Button from '@mui/material/Button';
4
+ import Chip from '@mui/material/Chip';
5
+ import Dialog from '@mui/material/Dialog';
6
+ import DialogActions from '@mui/material/DialogActions';
7
+ import DialogContent from '@mui/material/DialogContent';
8
+ import DialogTitle from '@mui/material/DialogTitle';
9
+ import FormControlLabel from '@mui/material/FormControlLabel';
10
+ import IconButton from '@mui/material/IconButton';
11
+ import Switch from '@mui/material/Switch';
12
+ import Tab from '@mui/material/Tab';
13
+ import Tabs from '@mui/material/Tabs';
14
+ import Tooltip from '@mui/material/Tooltip';
15
+ import Typography from '@mui/material/Typography';
16
+ import { useTheme } from '@mui/material/styles';
17
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
18
+ import CheckIcon from '@mui/icons-material/Check';
19
+ import StorageIcon from '@mui/icons-material/Storage';
20
+ import { useApi, discoveryApiRef } from '@backstage/core-plugin-api';
21
+ import { ToolIcon } from '../ToolIcon';
22
+ import { useCopyToClipboard, useProviders } from '../../hooks';
23
+ import type { AiTool } from '@nospt/plugin-dev-ai-hub-common';
24
+
25
+ interface ToolConfig {
26
+ tool: AiTool;
27
+ label: string;
28
+ file: string;
29
+ description: string;
30
+ buildConfig: (mcpUrl: string) => string;
31
+ }
32
+
33
+ const TOOL_CONFIGS: ToolConfig[] = [
34
+ {
35
+ tool: 'claude-code',
36
+ label: 'Claude Code',
37
+ file: '.mcp.json',
38
+ description: 'Add to .mcp.json in your project root (or run via claude mcp add):',
39
+ buildConfig: url => JSON.stringify({
40
+ mcpServers: { 'dev-ai-hub': { type: 'http', url } },
41
+ }, null, 2),
42
+ },
43
+ {
44
+ tool: 'github-copilot',
45
+ label: 'GitHub Copilot',
46
+ file: '.vscode/settings.json',
47
+ description: 'Add to your VS Code settings (.vscode/settings.json or user settings):',
48
+ buildConfig: url => JSON.stringify({
49
+ 'github.copilot.chat.mcp.servers': { 'dev-ai-hub': { type: 'http', url } },
50
+ }, null, 2),
51
+ },
52
+ {
53
+ tool: 'google-gemini',
54
+ label: 'Google Gemini',
55
+ file: 'gemini-config.json',
56
+ description: 'Add to your Gemini CLI configuration:',
57
+ buildConfig: url => JSON.stringify({
58
+ mcpServers: { 'dev-ai-hub': { url } },
59
+ }, null, 2),
60
+ },
61
+ ];
62
+
63
+ function providerLabel(target: string): string {
64
+ return target.split('/').pop()?.replace(/\.git$/, '') ?? target;
65
+ }
66
+
67
+ interface McpConfigDialogProps {
68
+ open: boolean;
69
+ onClose: () => void;
70
+ }
71
+
72
+ export function McpConfigDialog({ open, onClose }: McpConfigDialogProps) {
73
+ const theme = useTheme();
74
+ const discoveryApi = useApi(discoveryApiRef);
75
+ const { copy: copyUrl, copied: copiedUrl } = useCopyToClipboard();
76
+ const { copy: copySnippet, copied: copiedSnippet } = useCopyToClipboard();
77
+ const [tab, setTab] = useState(0);
78
+ const [baseUrl, setBaseUrl] = useState('');
79
+ const [selectedProvider, setSelectedProvider] = useState<string>('');
80
+ const [proactiveEnabled, setProactiveEnabled] = useState(false);
81
+
82
+ const { providers } = useProviders();
83
+ const showProviderFilter = providers.length > 1;
84
+
85
+ useEffect(() => {
86
+ if (open) {
87
+ discoveryApi.getBaseUrl('dev-ai-hub').then(url => setBaseUrl(url));
88
+ }
89
+ }, [open, discoveryApi]);
90
+
91
+ const cfg = TOOL_CONFIGS[tab];
92
+
93
+ const buildMcpUrl = () => {
94
+ if (!baseUrl) return 'loading...';
95
+ const params = new URLSearchParams();
96
+ params.set('tool', cfg.tool);
97
+ if (selectedProvider) params.set('provider', selectedProvider);
98
+ if (proactiveEnabled) params.set('proactive', 'true');
99
+ return `${baseUrl}/mcp?${params.toString()}`;
100
+ };
101
+
102
+ const mcpUrl = buildMcpUrl();
103
+ const configSnippet = baseUrl ? cfg.buildConfig(mcpUrl) : '';
104
+
105
+ return (
106
+ <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
107
+ <DialogTitle sx={{ pb: 0 }}>
108
+ <Typography variant="h6" fontWeight={700}>Configure MCP Server</Typography>
109
+ <Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
110
+ Connect your AI tool to the Dev AI Hub via Model Context Protocol.
111
+ </Typography>
112
+ </DialogTitle>
113
+
114
+ <DialogContent sx={{ pt: 1 }}>
115
+ <Tabs
116
+ value={tab}
117
+ onChange={(_, v) => setTab(v)}
118
+ sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
119
+ >
120
+ {TOOL_CONFIGS.map((t, i) => (
121
+ <Tab
122
+ key={t.tool}
123
+ value={i}
124
+ label={
125
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
126
+ <ToolIcon
127
+ tool={t.tool}
128
+ sx={{ fontSize: '1rem', color: theme.palette.mode === 'dark' ? '#fff' : undefined }}
129
+ />
130
+ <span>{t.label}</span>
131
+ </Box>
132
+ }
133
+ />
134
+ ))}
135
+ </Tabs>
136
+
137
+ {/* Provider filter — only shown when there are 2+ providers */}
138
+ {showProviderFilter && (
139
+ <Box sx={{ mb: 2 }}>
140
+ <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', mb: 0.75 }}>
141
+ Scope to Provider
142
+ </Typography>
143
+ <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
144
+ <Chip
145
+ label="All providers"
146
+ size="small"
147
+ clickable
148
+ onClick={() => setSelectedProvider('')}
149
+ sx={{
150
+ fontWeight: 600,
151
+ fontSize: '0.75rem',
152
+ borderRadius: 2,
153
+ border: '1.5px solid',
154
+ borderColor: !selectedProvider ? 'text.primary' : 'divider',
155
+ backgroundColor: !selectedProvider ? 'text.primary' : 'transparent',
156
+ color: !selectedProvider ? 'background.paper' : 'text.secondary',
157
+ transition: 'all 0.15s ease',
158
+ }}
159
+ />
160
+ {providers.map(p => {
161
+ const isSelected = selectedProvider === p.id;
162
+ const label = providerLabel(p.target);
163
+ return (
164
+ <Chip
165
+ key={p.id}
166
+ icon={<StorageIcon sx={{ fontSize: '0.8rem !important', color: isSelected ? 'background.paper' : 'inherit' }} />}
167
+ label={label}
168
+ size="small"
169
+ clickable
170
+ onClick={() => setSelectedProvider(isSelected ? '' : p.id)}
171
+ sx={{
172
+ fontWeight: 600,
173
+ fontSize: '0.75rem',
174
+ borderRadius: 2,
175
+ border: '1.5px solid',
176
+ borderColor: isSelected ? 'text.primary' : 'divider',
177
+ backgroundColor: isSelected ? 'text.primary' : 'transparent',
178
+ color: isSelected ? 'background.paper' : 'text.secondary',
179
+ transition: 'all 0.15s ease',
180
+ }}
181
+ />
182
+ );
183
+ })}
184
+ </Box>
185
+ </Box>
186
+ )}
187
+
188
+ {/* Proactive suggestions toggle */}
189
+ <Box sx={{ mb: 2 }}>
190
+ <FormControlLabel
191
+ control={
192
+ <Switch
193
+ size="small"
194
+ checked={proactiveEnabled}
195
+ onChange={e => setProactiveEnabled(e.target.checked)}
196
+ />
197
+ }
198
+ label={
199
+ <Box>
200
+ <Typography variant="body2" fontWeight={600}>Proactive suggestions</Typography>
201
+ <Typography variant="caption" color="text.secondary">
202
+ The AI will automatically suggest relevant assets based on your project context.
203
+ Disable if you prefer to search manually.
204
+ </Typography>
205
+ </Box>
206
+ }
207
+ sx={{ alignItems: 'flex-start', ml: 0, gap: 1 }}
208
+ />
209
+ </Box>
210
+
211
+ {/* MCP URL */}
212
+ <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
213
+ MCP Endpoint
214
+ </Typography>
215
+ <Box
216
+ sx={{
217
+ display: 'flex',
218
+ alignItems: 'center',
219
+ gap: 1,
220
+ mt: 0.5,
221
+ mb: 2,
222
+ px: 1.5,
223
+ py: 1,
224
+ borderRadius: 1.5,
225
+ border: '1px solid',
226
+ borderColor: 'divider',
227
+ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
228
+ }}
229
+ >
230
+ <Typography
231
+ variant="body2"
232
+ sx={{ flex: 1, fontFamily: 'monospace', fontSize: '0.8rem', wordBreak: 'break-all' }}
233
+ >
234
+ {mcpUrl}
235
+ </Typography>
236
+ <Tooltip title={copiedUrl ? 'Copied!' : 'Copy URL'}>
237
+ <IconButton size="small" onClick={() => copyUrl(mcpUrl)}>
238
+ {copiedUrl ? <CheckIcon fontSize="small" color="success" /> : <ContentCopyIcon fontSize="small" />}
239
+ </IconButton>
240
+ </Tooltip>
241
+ </Box>
242
+
243
+ {/* Config snippet */}
244
+ <Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
245
+ {cfg.file}
246
+ </Typography>
247
+ <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1, mt: 0.5 }}>
248
+ {cfg.description}
249
+ </Typography>
250
+ <Box sx={{ position: 'relative' }}>
251
+ <Box
252
+ component="pre"
253
+ sx={{
254
+ m: 0,
255
+ p: 2,
256
+ borderRadius: 2,
257
+ border: '1px solid',
258
+ borderColor: 'divider',
259
+ backgroundColor: theme.palette.mode === 'dark' ? '#0d1117' : '#f6f8fa',
260
+ color: theme.palette.mode === 'dark' ? '#e6edf3' : '#24292f',
261
+ fontFamily: 'monospace',
262
+ fontSize: '0.8rem',
263
+ overflowX: 'auto',
264
+ whiteSpace: 'pre',
265
+ }}
266
+ >
267
+ {configSnippet}
268
+ </Box>
269
+ <Tooltip title={copiedSnippet ? 'Copied!' : 'Copy config'}>
270
+ <IconButton
271
+ size="small"
272
+ onClick={() => copySnippet(configSnippet)}
273
+ sx={{
274
+ position: 'absolute',
275
+ top: 8,
276
+ right: 8,
277
+ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)',
278
+ '&:hover': {
279
+ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.12)',
280
+ },
281
+ }}
282
+ >
283
+ {copiedSnippet ? <CheckIcon fontSize="small" color="success" /> : <ContentCopyIcon fontSize="small" />}
284
+ </IconButton>
285
+ </Tooltip>
286
+ </Box>
287
+
288
+ <Typography variant="caption" color="text.disabled" sx={{ display: 'block', mt: 1.5 }}>
289
+ 💡 Omit <code>?tool=</code> from the URL to receive assets for all AI tools.
290
+ {showProviderFilter && ' Omit ?provider= to receive assets from all repositories.'}
291
+ {' '}Proactive suggestions add <code>?proactive=true</code> and register the{' '}
292
+ <code>suggest_assets</code> tool and <code>check_for_assets</code> prompt.
293
+ </Typography>
294
+ </DialogContent>
295
+
296
+ <DialogActions>
297
+ <Button onClick={onClose}>Close</Button>
298
+ </DialogActions>
299
+ </Dialog>
300
+ );
301
+ }
@@ -0,0 +1 @@
1
+ export { McpConfigDialog } from './McpConfigDialog';