@nospt/plugin-dev-ai-hub 0.2.4 → 0.3.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 (122) hide show
  1. package/dist/api/DevAiHubResourceClient.esm.js +97 -0
  2. package/dist/api/DevAiHubResourceClient.esm.js.map +1 -0
  3. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +150 -0
  4. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -0
  5. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +8 -0
  6. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js.map +1 -0
  7. package/dist/components/AiResourcesPage/ResourceCard.esm.js +158 -0
  8. package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -0
  9. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +8 -0
  10. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js.map +1 -0
  11. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +234 -0
  12. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -0
  13. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +8 -0
  14. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js.map +1 -0
  15. package/dist/components/AiResourcesPage/ResourceFilters.esm.js +242 -0
  16. package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -0
  17. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js +8 -0
  18. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js.map +1 -0
  19. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +366 -0
  20. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -0
  21. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js +8 -0
  22. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js.map +1 -0
  23. package/dist/components/AiResourcesPage/StatTiles.esm.js +49 -0
  24. package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -0
  25. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +8 -0
  26. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js.map +1 -0
  27. package/dist/components/AiResourcesPage/index.esm.js +6 -0
  28. package/dist/components/AiResourcesPage/typeMeta.esm.js +35 -0
  29. package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -0
  30. package/dist/components/ToolIcon/ToolIcon.esm.js +26 -9
  31. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  32. package/dist/hooks/useResourceBody.esm.js +39 -0
  33. package/dist/hooks/useResourceBody.esm.js.map +1 -0
  34. package/dist/hooks/useResources.esm.js +26 -0
  35. package/dist/hooks/useResources.esm.js.map +1 -0
  36. package/dist/hooks/useTelemetryCounts.esm.js +24 -0
  37. package/dist/hooks/useTelemetryCounts.esm.js.map +1 -0
  38. package/dist/index.d.ts +1 -38
  39. package/dist/index.esm.js +1 -3
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/plugin.esm.js +15 -24
  42. package/dist/plugin.esm.js.map +1 -1
  43. package/dist/routes.esm.js +1 -2
  44. package/dist/routes.esm.js.map +1 -1
  45. package/package.json +17 -10
  46. package/.eslintrc.js +0 -1
  47. package/dev/index.tsx +0 -32
  48. package/dist/api/DevAiHubClient.esm.js +0 -78
  49. package/dist/api/DevAiHubClient.esm.js.map +0 -1
  50. package/dist/components/AssetCard/AssetCard.esm.js +0 -120
  51. package/dist/components/AssetCard/AssetCard.esm.js.map +0 -1
  52. package/dist/components/AssetCard/AssetCard.module.css.esm.js +0 -8
  53. package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +0 -1
  54. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +0 -218
  55. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +0 -1
  56. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +0 -8
  57. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +0 -1
  58. package/dist/components/AssetFilters/AssetFilters.esm.js +0 -176
  59. package/dist/components/AssetFilters/AssetFilters.esm.js.map +0 -1
  60. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +0 -8
  61. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +0 -1
  62. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +0 -138
  63. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +0 -1
  64. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +0 -8
  65. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +0 -1
  66. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +0 -195
  67. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +0 -1
  68. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +0 -8
  69. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +0 -1
  70. package/dist/components/DevAiHubPage/index.esm.js +0 -6
  71. package/dist/components/McpConfigPage/McpConfigPage.esm.js +0 -200
  72. package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +0 -1
  73. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +0 -8
  74. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +0 -1
  75. package/dist/components/McpConfigPage/index.esm.js +0 -6
  76. package/dist/components/McpConfigPage/index.esm.js.map +0 -1
  77. package/dist/hooks/index.esm.js +0 -114
  78. package/dist/hooks/index.esm.js.map +0 -1
  79. package/dist/pluginLegacy.esm.js +0 -27
  80. package/dist/pluginLegacy.esm.js.map +0 -1
  81. package/package.json-prepack +0 -85
  82. package/src/api/DevAiHubClient.ts +0 -110
  83. package/src/components/AssetCard/AssetCard.module.css +0 -153
  84. package/src/components/AssetCard/AssetCard.test.tsx +0 -182
  85. package/src/components/AssetCard/AssetCard.tsx +0 -152
  86. package/src/components/AssetCard/index.ts +0 -1
  87. package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +0 -207
  88. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +0 -313
  89. package/src/components/AssetDetailPanel/index.ts +0 -1
  90. package/src/components/AssetFilters/AssetFilters.module.css +0 -67
  91. package/src/components/AssetFilters/AssetFilters.tsx +0 -219
  92. package/src/components/AssetFilters/index.ts +0 -2
  93. package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +0 -72
  94. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +0 -176
  95. package/src/components/AssetInstallDialog/index.ts +0 -1
  96. package/src/components/DevAiHubPage/DevAiHubPage.module.css +0 -105
  97. package/src/components/DevAiHubPage/DevAiHubPage.tsx +0 -197
  98. package/src/components/DevAiHubPage/index.ts +0 -1
  99. package/src/components/McpConfigDialog/McpConfigDialog.module.css +0 -111
  100. package/src/components/McpConfigDialog/McpConfigDialog.tsx +0 -271
  101. package/src/components/McpConfigDialog/index.ts +0 -1
  102. package/src/components/McpConfigPage/McpConfigPage.module.css +0 -121
  103. package/src/components/McpConfigPage/McpConfigPage.tsx +0 -242
  104. package/src/components/McpConfigPage/index.ts +0 -1
  105. package/src/components/ToolIcon/ToolIcon.tsx +0 -65
  106. package/src/components/ToolIcon/index.ts +0 -1
  107. package/src/css-modules.d.ts +0 -4
  108. package/src/hooks/index.ts +0 -152
  109. package/src/index.ts +0 -10
  110. package/src/plugin.tsx +0 -60
  111. package/src/pluginLegacy.ts +0 -42
  112. package/src/routes.ts +0 -4
  113. package/tests/e2e/.auth.json +0 -14
  114. package/tests/e2e/asset-detail.spec.ts +0 -90
  115. package/tests/e2e/asset-filtering.spec.ts +0 -145
  116. package/tests/e2e/asset-installation.spec.ts +0 -74
  117. package/tests/e2e/dev-ai-hub-page.spec.ts +0 -89
  118. package/tests/e2e/fixtures/base.ts +0 -61
  119. package/tests/e2e/fixtures/mock-api.ts +0 -221
  120. package/tests/e2e/mcp-config.spec.ts +0 -88
  121. package/tsconfig.json +0 -10
  122. /package/dist/components/{DevAiHubPage → AiResourcesPage}/index.esm.js.map +0 -0
@@ -1,197 +0,0 @@
1
- import { useState, useMemo, type ElementType } from 'react';
2
- import { useSearchParams } from 'react-router-dom';
3
- import { Box, Flex, Text, Skeleton, TablePagination } from '@backstage/ui';
4
- import { RiArticleLine, RiRobot2Line, RiToolsLine, RiGitBranchLine } from '@remixicon/react';
5
- import type { AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
6
- import { AssetCard } from '../AssetCard';
7
- import { AssetFilters } from '../AssetFilters';
8
- import type { AssetFiltersValue } from '../AssetFilters';
9
- import { AssetDetailPanel } from '../AssetDetailPanel';
10
- import { AssetInstallDialog } from '../AssetInstallDialog';
11
- import { useAssets, useStats, useProviders } from '../../hooks';
12
- import styles from './DevAiHubPage.module.css';
13
-
14
- const DEFAULT_FILTERS: AssetFiltersValue = {
15
- types: [],
16
- tools: [],
17
- search: '',
18
- tags: [],
19
- providerId: undefined,
20
- };
21
-
22
- const PAGE_SIZE = 24;
23
-
24
- const STATS_CONFIG: { key: AssetType; label: string; Icon: ElementType; gradient: string; shadow: string }[] = [
25
- { key: 'instruction', label: 'Instructions', Icon: RiArticleLine,
26
- gradient: 'linear-gradient(135deg, #54A0FF 0%, #2980FF 100%)', shadow: '#54A0FF40' },
27
- { key: 'agent', label: 'Agents', Icon: RiRobot2Line,
28
- gradient: 'linear-gradient(135deg, #FF6B9D 0%, #E0507A 100%)', shadow: '#FF6B9D40' },
29
- { key: 'skill', label: 'Skills', Icon: RiToolsLine,
30
- gradient: 'linear-gradient(135deg, #6AB04C 0%, #4A8F2E 100%)', shadow: '#6AB04C40' },
31
- { key: 'workflow', label: 'Workflows', Icon: RiGitBranchLine,
32
- gradient: 'linear-gradient(135deg, #F9CA24 0%, #D4A800 100%)', shadow: '#F9CA2440' },
33
- ];
34
-
35
- export function DevAiHubPage() {
36
- const [filters, setFilters] = useState<AssetFiltersValue>(DEFAULT_FILTERS);
37
- const [page, setPage] = useState(1);
38
-
39
- const [searchParams, setSearchParams] = useSearchParams();
40
- const selectedAssetId = searchParams.get('assetId');
41
- const installAssetId = searchParams.get('installId');
42
-
43
- const handleViewAsset = (id: string) =>
44
- setSearchParams(p => { const n = new URLSearchParams(p); n.set('assetId', id); return n; });
45
-
46
- const handleCloseDetail = () =>
47
- setSearchParams(p => { const n = new URLSearchParams(p); n.delete('assetId'); return n; });
48
-
49
- const handleInstallAsset = (id: string) =>
50
- setSearchParams(p => { const n = new URLSearchParams(p); n.set('installId', id); return n; });
51
-
52
- const handleCloseInstall = () =>
53
- setSearchParams(p => { const n = new URLSearchParams(p); n.delete('installId'); return n; });
54
-
55
- const { stats } = useStats();
56
- const { providers } = useProviders();
57
-
58
- const apiFilter = useMemo(
59
- () => ({
60
- type: filters.types.length === 1 ? (filters.types[0] as AssetType) : undefined,
61
- tool: filters.tools.length === 1 ? (filters.tools[0] as AiTool) : undefined,
62
- search: filters.search || undefined,
63
- tags: filters.tags.length > 0 ? filters.tags : undefined,
64
- providerId: filters.providerId || undefined,
65
- page,
66
- pageSize: PAGE_SIZE,
67
- }),
68
- [filters, page],
69
- );
70
-
71
- const { result, loading } = useAssets(apiFilter);
72
-
73
- const handleFiltersChange = (next: AssetFiltersValue) => {
74
- setFilters(next);
75
- setPage(1);
76
- };
77
-
78
- const totalPages = result ? Math.ceil(result.totalCount / PAGE_SIZE) : 0;
79
-
80
- const availableTags = useMemo(() => {
81
- if (!result) return [];
82
- const tagSet = new Set<string>();
83
- result.items.forEach(a => a.tags.forEach(t => tagSet.add(t)));
84
- return Array.from(tagSet).sort();
85
- }, [result]);
86
-
87
- return (
88
- <div className={styles.pageRoot}>
89
- <div className={styles.content}>
90
- {/* Stats row */}
91
- <div className={styles.statsGrid}>
92
- {STATS_CONFIG.map(({ key, label, Icon, gradient, shadow }) => {
93
- const isActive = filters.types[0] === key;
94
- return (
95
- <div
96
- key={key}
97
- className={styles.statCard}
98
- onClick={() => handleFiltersChange({ ...filters, types: isActive ? [] : [key] })}
99
- style={{
100
- background: gradient,
101
- boxShadow: isActive ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
102
- transform: isActive ? 'translateY(-2px)' : 'none',
103
- outline: isActive ? '2px solid rgba(255,255,255,0.6)' : 'none',
104
- outlineOffset: 2,
105
- }}
106
- role="button"
107
- tabIndex={0}
108
- onKeyDown={e => { if (e.key === 'Enter') handleFiltersChange({ ...filters, types: isActive ? [] : [key] }); }}
109
- >
110
- <div className={styles.statCardInner}>
111
- <Box>
112
- <Text variant="title-large" weight="bold" className={styles.statValue}>
113
- {stats ? (stats.byType[key] ?? 0) : <Skeleton style={{ width: 32 }} />}
114
- </Text>
115
- <Text variant="body-small" className={styles.statLabel}>
116
- {label}
117
- </Text>
118
- </Box>
119
- <div className={styles.statIconBox}>
120
- <Icon size={22} style={{ color: '#fff' }} />
121
- </div>
122
- </div>
123
- </div>
124
- );
125
- })}
126
- </div>
127
-
128
- {/* Filters */}
129
- <AssetFilters
130
- value={filters}
131
- onChange={handleFiltersChange}
132
- availableTags={availableTags}
133
- providers={providers.length > 1 ? providers : undefined}
134
- />
135
-
136
- {/* Results summary */}
137
- {result && !loading && (
138
- <Text variant="body-x-small" color="secondary" style={{ marginBottom: 'var(--bui-space-4)', display: 'block' }}>
139
- {result.totalCount} asset{result.totalCount !== 1 ? 's' : ''} found
140
- </Text>
141
- )}
142
-
143
- {/* Asset grid — 4 columns on large screens */}
144
- <div className={styles.assetGrid}>
145
- {loading
146
- ? Array.from({ length: 8 }).map((_, i) => (
147
- <Skeleton key={i} style={{ height: 150, borderRadius: 'var(--bui-radius-2)' }} />
148
- ))
149
- : result?.items.map(asset => (
150
- <AssetCard
151
- key={asset.id}
152
- asset={asset}
153
- onView={handleViewAsset}
154
- onInstall={handleInstallAsset}
155
- />
156
- ))}
157
- </div>
158
-
159
- {/* Empty state */}
160
- {!loading && result?.items.length === 0 && (
161
- <div className={styles.emptyState}>
162
- <div className={styles.emptyEmoji}>🤖</div>
163
- <Text variant="title-small" color="secondary" weight="bold">No assets found</Text>
164
- <Text variant="body-small" color="secondary" style={{ marginTop: 'var(--bui-space-1)' }}>
165
- Try adjusting your filters or search terms.
166
- </Text>
167
- </div>
168
- )}
169
-
170
- {/* Pagination */}
171
- {totalPages > 1 && (
172
- <Flex className={styles.paginationRow}>
173
- <TablePagination
174
- pageSize={PAGE_SIZE}
175
- offset={(page - 1) * PAGE_SIZE}
176
- totalCount={result?.totalCount ?? 0}
177
- hasNextPage={page < totalPages}
178
- hasPreviousPage={page > 1}
179
- onNextPage={() => setPage(p => p + 1)}
180
- onPreviousPage={() => setPage(p => p - 1)}
181
- />
182
- </Flex>
183
- )}
184
- </div>
185
-
186
- <AssetDetailPanel
187
- assetId={selectedAssetId}
188
- onClose={handleCloseDetail}
189
- />
190
-
191
- <AssetInstallDialog
192
- assetId={installAssetId}
193
- onClose={handleCloseInstall}
194
- />
195
- </div>
196
- );
197
- }
@@ -1 +0,0 @@
1
- export { DevAiHubPage } from './DevAiHubPage';
@@ -1,111 +0,0 @@
1
- @layer components {
2
- .subtitle {
3
- margin-top: var(--bui-space-1);
4
- }
5
-
6
- .tabBar {
7
- margin-bottom: var(--bui-space-4);
8
- border-bottom: 1px solid var(--bui-border-1);
9
- }
10
-
11
- .tabContent {
12
- display: flex;
13
- align-items: center;
14
- gap: var(--bui-space-2);
15
- }
16
-
17
- .sectionLabel {
18
- text-transform: uppercase;
19
- letter-spacing: 0.5px;
20
- display: block;
21
- margin-bottom: 6px;
22
- font-weight: 600;
23
- }
24
-
25
- .providerSection {
26
- margin-bottom: var(--bui-space-4);
27
- }
28
-
29
- .chipsRow {
30
- display: flex;
31
- gap: 6px;
32
- flex-wrap: wrap;
33
- }
34
-
35
- .filterChip {
36
- font-weight: 600;
37
- font-size: 0.75rem;
38
- border-radius: var(--bui-radius-2);
39
- border: 1.5px solid;
40
- transition: all 0.15s ease;
41
- cursor: pointer;
42
- }
43
-
44
- .proactiveSection {
45
- margin-bottom: var(--bui-space-4);
46
- }
47
-
48
- .proactiveRow {
49
- display: flex;
50
- align-items: flex-start;
51
- gap: var(--bui-space-2);
52
- }
53
-
54
- .urlSection {
55
- margin-bottom: var(--bui-space-4);
56
- }
57
-
58
- .urlBox {
59
- display: flex;
60
- align-items: center;
61
- gap: var(--bui-space-2);
62
- margin-top: var(--bui-space-1);
63
- padding: var(--bui-space-2) var(--bui-space-3);
64
- border-radius: var(--bui-radius-2);
65
- border: 1px solid var(--bui-border-1);
66
- background-color: var(--bui-bg-neutral-1-hover);
67
- }
68
-
69
- .urlText {
70
- flex: 1;
71
- font-family: monospace;
72
- font-size: 0.8rem;
73
- word-break: break-all;
74
- }
75
-
76
- .snippetContainer {
77
- position: relative;
78
- }
79
-
80
- .snippetPre {
81
- margin: 0;
82
- padding: var(--bui-space-4);
83
- border-radius: var(--bui-radius-2);
84
- border: 1px solid var(--bui-border-1);
85
- font-family: monospace;
86
- font-size: 0.8rem;
87
- overflow-x: auto;
88
- white-space: pre;
89
- }
90
-
91
- .snippetPreLight {
92
- background-color: #f6f8fa;
93
- color: #24292f;
94
- }
95
-
96
- .snippetPreDark {
97
- background-color: #1E1E1E;
98
- color: #DCDDE1;
99
- }
100
-
101
- .copySnippetButton {
102
- position: absolute;
103
- top: 8px;
104
- right: 8px;
105
- }
106
-
107
- .hint {
108
- display: block;
109
- margin-top: var(--bui-space-3);
110
- }
111
- }
@@ -1,271 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import {
3
- Box, Flex, Text, Button, ButtonIcon, ToggleButton, Switch,
4
- Dialog, DialogTrigger, DialogHeader, DialogBody, DialogFooter,
5
- Tabs, TabList, Tab, TabPanel,
6
- Tooltip, TooltipTrigger,
7
- } from '@backstage/ui';
8
- import { RiFileCopyLine, RiCheckLine, RiDatabase2Line } from '@remixicon/react';
9
- import { useApi, discoveryApiRef } from '@backstage/core-plugin-api';
10
- import { ToolIcon } from '../ToolIcon';
11
- import { useCopyToClipboard, useProviders } from '../../hooks';
12
- import type { AiTool } from '@nospt/plugin-dev-ai-hub-common';
13
- import styles from './McpConfigDialog.module.css';
14
-
15
- interface ToolConfig {
16
- tool: AiTool;
17
- label: string;
18
- file: string;
19
- description: string;
20
- buildConfig: (mcpUrl: string) => string;
21
- }
22
-
23
- const TOOL_CONFIGS: ToolConfig[] = [
24
- {
25
- tool: 'claude-code',
26
- label: 'Claude Code',
27
- file: '.mcp.json',
28
- description: 'Add to .mcp.json in your project root (or run via claude mcp add):',
29
- buildConfig: url => JSON.stringify({
30
- mcpServers: { 'dev-ai-hub': { type: 'http', url } },
31
- }, null, 2),
32
- },
33
- {
34
- tool: 'github-copilot',
35
- label: 'GitHub Copilot',
36
- file: '.vscode/settings.json',
37
- description: 'Add to your VS Code settings (.vscode/settings.json or user settings):',
38
- buildConfig: url => JSON.stringify({
39
- 'github.copilot.chat.mcp.servers': { 'dev-ai-hub': { type: 'http', url } },
40
- }, null, 2),
41
- },
42
- {
43
- tool: 'google-gemini',
44
- label: 'Google Gemini',
45
- file: 'gemini-config.json',
46
- description: 'Add to your Gemini CLI configuration:',
47
- buildConfig: url => JSON.stringify({
48
- mcpServers: { 'dev-ai-hub': { url } },
49
- }, null, 2),
50
- },
51
- ];
52
-
53
- function providerLabel(target: string): string {
54
- return target.split('/').pop()?.replace(/\.git$/, '') ?? target;
55
- }
56
-
57
- interface McpConfigDialogProps {
58
- open: boolean;
59
- onClose: () => void;
60
- }
61
-
62
- export function McpConfigDialog({ open, onClose }: McpConfigDialogProps) {
63
- const discoveryApi = useApi(discoveryApiRef);
64
- const { copy: copyUrl, copied: copiedUrl } = useCopyToClipboard();
65
- const { copy: copySnippet, copied: copiedSnippet } = useCopyToClipboard();
66
- const [selectedToolKey, setSelectedToolKey] = useState<string>('claude-code');
67
- const [baseUrl, setBaseUrl] = useState('');
68
- const [selectedProvider, setSelectedProvider] = useState<string>('');
69
- const [proactiveEnabled, setProactiveEnabled] = useState(false);
70
-
71
- const { providers } = useProviders();
72
- const showProviderFilter = providers.length > 1;
73
-
74
- useEffect(() => {
75
- if (open) {
76
- discoveryApi.getBaseUrl('dev-ai-hub').then(url => setBaseUrl(url));
77
- }
78
- }, [open, discoveryApi]);
79
-
80
- const cfg = TOOL_CONFIGS.find(t => t.tool === selectedToolKey) ?? TOOL_CONFIGS[0];
81
-
82
- const buildMcpUrl = () => {
83
- if (!baseUrl) return 'loading...';
84
- const params = new URLSearchParams();
85
- params.set('tool', cfg.tool);
86
- if (selectedProvider) params.set('provider', selectedProvider);
87
- if (proactiveEnabled) params.set('proactive', 'true');
88
- return `${baseUrl}/mcp?${params.toString()}`;
89
- };
90
-
91
- const mcpUrl = buildMcpUrl();
92
- const configSnippet = baseUrl ? cfg.buildConfig(mcpUrl) : '';
93
-
94
- // Detect dark mode by observing BUI's data-theme-mode attribute, which
95
- // reflects the Backstage theme toggle (not the OS preference).
96
- const [isDark, setIsDark] = useState(() => {
97
- if (typeof document === 'undefined') return false;
98
- return document.documentElement.getAttribute('data-theme-mode') === 'dark' ||
99
- document.body.getAttribute('data-theme-mode') === 'dark';
100
- });
101
-
102
- useEffect(() => {
103
- const getIsDark = () =>
104
- document.documentElement.getAttribute('data-theme-mode') === 'dark' ||
105
- document.body.getAttribute('data-theme-mode') === 'dark';
106
-
107
- const observer = new MutationObserver(() => setIsDark(getIsDark()));
108
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme-mode'] });
109
- observer.observe(document.body, { attributes: true, attributeFilter: ['data-theme-mode'] });
110
- return () => observer.disconnect();
111
- }, []);
112
-
113
- return (
114
- <DialogTrigger>
115
- <Dialog
116
- isOpen={open}
117
- isDismissable
118
- onOpenChange={o => { if (!o) onClose(); }}
119
- >
120
- <DialogHeader>
121
- <Text variant="title-small" weight="bold">Configure MCP Server</Text>
122
- <Text variant="body-small" color="secondary" className={styles.subtitle}>
123
- Connect your AI tool to the Dev AI Hub via Model Context Protocol.
124
- </Text>
125
- </DialogHeader>
126
-
127
- <DialogBody>
128
- <Tabs selectedKey={selectedToolKey} onSelectionChange={key => setSelectedToolKey(key as string)}>
129
- <TabList className={styles.tabBar}>
130
- {TOOL_CONFIGS.map(t => (
131
- <Tab key={t.tool} id={t.tool}>
132
- <span className={styles.tabContent}>
133
- <ToolIcon tool={t.tool} size={16} />
134
- <span>{t.label}</span>
135
- </span>
136
- </Tab>
137
- ))}
138
- </TabList>
139
-
140
- {/* All tab panels share the same content below */}
141
- {TOOL_CONFIGS.map(t => (
142
- <TabPanel key={t.tool} id={t.tool}>
143
- {/* Provider filter — only shown when there are 2+ providers */}
144
- {showProviderFilter && (
145
- <Box className={styles.providerSection}>
146
- <Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
147
- Scope to Provider
148
- </Text>
149
- <Flex className={styles.chipsRow}>
150
- <ToggleButton
151
- id="all-providers"
152
- size="small"
153
- className={styles.filterChip}
154
- isSelected={!selectedProvider}
155
- onChange={() => setSelectedProvider('')}
156
- style={{
157
- borderColor: !selectedProvider ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
158
- backgroundColor: !selectedProvider ? 'var(--bui-fg-primary)' : 'transparent',
159
- color: !selectedProvider ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
160
- }}
161
- >
162
- All providers
163
- </ToggleButton>
164
- {providers.map(p => {
165
- const isSelected = selectedProvider === p.id;
166
- const label = providerLabel(p.target);
167
- return (
168
- <ToggleButton
169
- key={p.id}
170
- id={p.id}
171
- size="small"
172
- className={styles.filterChip}
173
- iconStart={<RiDatabase2Line size={12} style={{ color: isSelected ? 'var(--bui-bg-neutral-1)' : 'inherit' }} />}
174
- isSelected={isSelected}
175
- onChange={() => setSelectedProvider(isSelected ? '' : p.id)}
176
- style={{
177
- borderColor: isSelected ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
178
- backgroundColor: isSelected ? 'var(--bui-fg-primary)' : 'transparent',
179
- color: isSelected ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
180
- }}
181
- >
182
- {label}
183
- </ToggleButton>
184
- );
185
- })}
186
- </Flex>
187
- </Box>
188
- )}
189
-
190
- {/* Proactive suggestions toggle */}
191
- <Flex className={styles.proactiveSection}>
192
- <Flex className={styles.proactiveRow}>
193
- <Switch
194
- isSelected={proactiveEnabled}
195
- onChange={setProactiveEnabled}
196
- aria-label="Proactive suggestions"
197
- />
198
- <Box>
199
- <Text variant="body-small" weight="bold">Proactive suggestions</Text>
200
- <Text variant="body-x-small" color="secondary">
201
- The AI will automatically suggest relevant assets based on your project context.
202
- Disable if you prefer to search manually.
203
- </Text>
204
- </Box>
205
- </Flex>
206
- </Flex>
207
-
208
- {/* MCP URL */}
209
- <Box className={styles.urlSection}>
210
- <Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
211
- MCP Endpoint
212
- </Text>
213
- <Flex className={styles.urlBox}>
214
- <Text variant="body-small" className={styles.urlText}>
215
- {mcpUrl}
216
- </Text>
217
- <TooltipTrigger>
218
- <ButtonIcon
219
- aria-label="Copy URL"
220
- icon={copiedUrl ? <RiCheckLine size={14} style={{ color: 'var(--bui-fg-success)' }} /> : <RiFileCopyLine size={14} />}
221
- variant="tertiary"
222
- onPress={() => copyUrl(mcpUrl)}
223
- />
224
- <Tooltip>{copiedUrl ? 'Copied!' : 'Copy URL'}</Tooltip>
225
- </TooltipTrigger>
226
- </Flex>
227
- </Box>
228
-
229
- {/* Config snippet */}
230
- <Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
231
- {t.file}
232
- </Text>
233
- <Text variant="body-x-small" color="secondary" style={{ display: 'block', marginBottom: 'var(--bui-space-2)', marginTop: 'var(--bui-space-1)' }}>
234
- {t.description}
235
- </Text>
236
- <div className={styles.snippetContainer}>
237
- <pre className={`${styles.snippetPre} ${isDark ? styles.snippetPreDark : styles.snippetPreLight}`}>
238
- {configSnippet}
239
- </pre>
240
- <TooltipTrigger>
241
- <ButtonIcon
242
- aria-label="Copy config"
243
- className={styles.copySnippetButton}
244
- icon={copiedSnippet ? <RiCheckLine size={14} style={{ color: 'var(--bui-fg-success)' }} /> : <RiFileCopyLine size={14} />}
245
- variant="tertiary"
246
- onPress={() => copySnippet(configSnippet)}
247
- />
248
- <Tooltip>{copiedSnippet ? 'Copied!' : 'Copy config'}</Tooltip>
249
- </TooltipTrigger>
250
- </div>
251
-
252
- <Text variant="body-x-small" color="secondary" className={styles.hint}>
253
- 💡 Omit <code>?tool=</code> from the URL to receive assets for all AI tools.
254
- {showProviderFilter && ' Omit ?provider= to receive assets from all repositories.'}
255
- {' '}Proactive suggestions add <code>?proactive=true</code> and register the{' '}
256
- <code>suggest_assets</code> tool and <code>check_for_assets</code> prompt.
257
- </Text>
258
- </TabPanel>
259
- ))}
260
- </Tabs>
261
- </DialogBody>
262
-
263
- <DialogFooter>
264
- <Button onClick={onClose} variant="secondary" slot="close">
265
- Close
266
- </Button>
267
- </DialogFooter>
268
- </Dialog>
269
- </DialogTrigger>
270
- );
271
- }
@@ -1 +0,0 @@
1
- export { McpConfigDialog } from './McpConfigDialog';