@terasky/backstage-plugin-ai-rules 1.6.0 → 1.8.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 (34) hide show
  1. package/config.d.ts +11 -9
  2. package/dist/alpha.esm.js +30 -0
  3. package/dist/alpha.esm.js.map +1 -1
  4. package/dist/api/AiRulesClient.esm.js +27 -19
  5. package/dist/api/AiRulesClient.esm.js.map +1 -1
  6. package/dist/api/types.esm.js.map +1 -1
  7. package/dist/components/AgentConfigsComponent/AgentConfigsComponent.esm.js +91 -0
  8. package/dist/components/AgentConfigsComponent/AgentConfigsComponent.esm.js.map +1 -0
  9. package/dist/components/AgentSkillsComponent/AgentSkillsComponent.esm.js +225 -0
  10. package/dist/components/AgentSkillsComponent/AgentSkillsComponent.esm.js.map +1 -0
  11. package/dist/components/AiInstructionsComponent/AiInstructionsComponent.esm.js +13 -3
  12. package/dist/components/AiInstructionsComponent/AiInstructionsComponent.esm.js.map +1 -1
  13. package/dist/components/AiRulesComponent/AiRulesComponent.esm.js +327 -248
  14. package/dist/components/AiRulesComponent/AiRulesComponent.esm.js.map +1 -1
  15. package/dist/components/IgnoreFilesComponent/IgnoreFilesComponent.esm.js +86 -0
  16. package/dist/components/IgnoreFilesComponent/IgnoreFilesComponent.esm.js.map +1 -0
  17. package/dist/components/MCPServersComponent/MCPServersComponent.esm.js +5 -1
  18. package/dist/components/MCPServersComponent/MCPServersComponent.esm.js.map +1 -1
  19. package/dist/hooks/useAgentConfigs.esm.js +38 -0
  20. package/dist/hooks/useAgentConfigs.esm.js.map +1 -0
  21. package/dist/hooks/useAiRules.esm.js +13 -1
  22. package/dist/hooks/useAiRules.esm.js.map +1 -1
  23. package/dist/hooks/useIgnoreFiles.esm.js +38 -0
  24. package/dist/hooks/useIgnoreFiles.esm.js.map +1 -0
  25. package/dist/hooks/useSkills.esm.js +38 -0
  26. package/dist/hooks/useSkills.esm.js.map +1 -0
  27. package/dist/index.d.ts +142 -5
  28. package/dist/index.esm.js +3 -0
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/plugin.esm.js +24 -0
  31. package/dist/plugin.esm.js.map +1 -1
  32. package/dist/types.esm.js +7 -0
  33. package/dist/types.esm.js.map +1 -1
  34. package/package.json +11 -11
@@ -1,23 +1,64 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useState, useMemo, useCallback } from 'react';
2
3
  import { useAiRules } from '../../hooks/useAiRules.esm.js';
3
- import { InfoCard, Progress, EmptyState, MarkdownContent } from '@backstage/core-components';
4
- import { makeStyles, Typography, FormControlLabel, Checkbox, Button, Card, CardContent, useTheme, Accordion, AccordionSummary, Chip, Tooltip, IconButton, AccordionDetails } from '@material-ui/core';
4
+ import { InfoCard, Progress, EmptyState, MarkdownContent, CodeSnippet } from '@backstage/core-components';
5
+ import { Typography, FormControlLabel, Checkbox, Button, TextField, Card, CardContent, Tooltip, makeStyles, useTheme, Accordion, AccordionSummary, Chip, IconButton, AccordionDetails, Snackbar } from '@material-ui/core';
6
+ import { ToggleButtonGroup, ToggleButton } from '@material-ui/lab';
5
7
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
6
8
  import CodeIcon from '@material-ui/icons/Code';
7
9
  import LaunchIcon from '@material-ui/icons/Launch';
10
+ import FileCopyIcon from '@material-ui/icons/FileCopy';
11
+ import GetAppIcon from '@material-ui/icons/GetApp';
8
12
  import { AIRuleType } from '../../types.esm.js';
9
13
 
10
14
  const isAIRulesAvailable = (entity) => {
11
15
  const sourceAnnotation = entity.metadata?.annotations?.["backstage.io/source-location"] || "";
12
16
  return sourceAnnotation.startsWith("url:");
13
17
  };
18
+ const RULE_TYPE_COLORS = {
19
+ [AIRuleType.CURSOR]: "#0066CC",
20
+ [AIRuleType.COPILOT]: "#6F42C1",
21
+ [AIRuleType.CLINE]: "#28A745",
22
+ [AIRuleType.CLAUDE_CODE]: "#FF6B35",
23
+ [AIRuleType.WINDSURF]: "#00B4D8",
24
+ [AIRuleType.ROO_CODE]: "#6610F2",
25
+ [AIRuleType.CODEX]: "#10A37F",
26
+ [AIRuleType.GEMINI]: "#4285F4",
27
+ [AIRuleType.AMAZON_Q]: "#FF9900",
28
+ [AIRuleType.CONTINUE]: "#1A73E8",
29
+ [AIRuleType.AIDER]: "#E83E8C"
30
+ };
31
+ const RULE_TYPE_DISPLAY_NAMES = {
32
+ [AIRuleType.CURSOR]: "Cursor",
33
+ [AIRuleType.COPILOT]: "Copilot",
34
+ [AIRuleType.CLINE]: "Cline",
35
+ [AIRuleType.CLAUDE_CODE]: "Claude Code",
36
+ [AIRuleType.WINDSURF]: "Windsurf",
37
+ [AIRuleType.ROO_CODE]: "Roo Code",
38
+ [AIRuleType.CODEX]: "OpenAI Codex",
39
+ [AIRuleType.GEMINI]: "Gemini CLI",
40
+ [AIRuleType.AMAZON_Q]: "Amazon Q",
41
+ [AIRuleType.CONTINUE]: "Continue",
42
+ [AIRuleType.AIDER]: "Aider"
43
+ };
44
+ const RULE_TYPE_DISPLAY_ORDER = [
45
+ AIRuleType.CURSOR,
46
+ AIRuleType.CLAUDE_CODE,
47
+ AIRuleType.COPILOT,
48
+ AIRuleType.CLINE,
49
+ AIRuleType.WINDSURF,
50
+ AIRuleType.ROO_CODE,
51
+ AIRuleType.CODEX,
52
+ AIRuleType.GEMINI,
53
+ AIRuleType.AMAZON_Q,
54
+ AIRuleType.CONTINUE,
55
+ AIRuleType.AIDER
56
+ ];
14
57
  const useStyles = makeStyles((theme) => ({
15
58
  root: {
16
59
  "& .MuiAccordion-root": {
17
60
  marginBottom: theme.spacing(1),
18
- "&:before": {
19
- display: "none"
20
- }
61
+ "&:before": { display: "none" }
21
62
  }
22
63
  },
23
64
  filterSection: {
@@ -26,6 +67,10 @@ const useStyles = makeStyles((theme) => ({
26
67
  backgroundColor: theme.palette.background.default,
27
68
  borderRadius: theme.shape.borderRadius
28
69
  },
70
+ searchBar: {
71
+ marginBottom: theme.spacing(2),
72
+ width: "100%"
73
+ },
29
74
  ruleCard: {
30
75
  marginBottom: theme.spacing(1),
31
76
  border: `1px solid ${theme.palette.divider}`
@@ -40,7 +85,14 @@ const useStyles = makeStyles((theme) => ({
40
85
  display: "flex",
41
86
  alignItems: "center",
42
87
  gap: theme.spacing(1),
43
- flex: 1
88
+ flex: 1,
89
+ overflow: "hidden"
90
+ },
91
+ ruleHeaderActions: {
92
+ display: "flex",
93
+ alignItems: "center",
94
+ gap: theme.spacing(0.5),
95
+ flexShrink: 0
44
96
  },
45
97
  ruleType: {
46
98
  textTransform: "uppercase",
@@ -52,9 +104,7 @@ const useStyles = makeStyles((theme) => ({
52
104
  borderRadius: theme.shape.borderRadius,
53
105
  overflow: "auto",
54
106
  maxHeight: "300px",
55
- "& > *": {
56
- backgroundColor: "transparent !important"
57
- }
107
+ "& > *": { backgroundColor: "transparent !important" }
58
108
  },
59
109
  ruleMetadata: {
60
110
  display: "flex",
@@ -64,88 +114,34 @@ const useStyles = makeStyles((theme) => ({
64
114
  },
65
115
  statsContainer: {
66
116
  display: "flex",
117
+ flexWrap: "wrap",
67
118
  gap: theme.spacing(2),
68
- marginBottom: theme.spacing(2)
119
+ marginBottom: theme.spacing(2),
120
+ alignItems: "center"
69
121
  },
70
122
  statCard: {
71
- minWidth: "120px",
123
+ minWidth: "100px",
72
124
  textAlign: "center"
73
125
  },
74
- emptyStateIcon: {
75
- fontSize: "4rem",
76
- color: theme.palette.grey[400]
77
- },
78
126
  filterContainer: {
79
127
  display: "flex",
80
128
  flexWrap: "wrap",
81
- "& > *": {
82
- marginRight: theme.spacing(1)
83
- }
129
+ "& > *": { marginRight: theme.spacing(1) }
84
130
  },
85
131
  applyFilterButton: {
86
132
  marginTop: theme.spacing(1)
133
+ },
134
+ viewToggle: {
135
+ marginBottom: theme.spacing(1)
136
+ },
137
+ exportButton: {
138
+ marginLeft: "auto"
87
139
  }
88
140
  }));
89
- const RuleTypeIcon = ({ type }) => {
90
- const colors = {
91
- [AIRuleType.CURSOR]: "#0066CC",
92
- [AIRuleType.COPILOT]: "#6F42C1",
93
- [AIRuleType.CLINE]: "#28A745",
94
- [AIRuleType.CLAUDE_CODE]: "#FF6B35"
95
- };
96
- return /* @__PURE__ */ jsx(CodeIcon, { style: { color: colors[type] } });
97
- };
98
- const renderFrontmatter = (theme, frontmatter) => {
99
- if (!frontmatter || Object.keys(frontmatter).length === 0) {
100
- return null;
101
- }
102
- const filteredEntries = Object.entries(frontmatter).filter(
103
- ([key]) => !["description", "globs"].includes(key)
104
- );
105
- if (filteredEntries.length === 0) {
106
- return null;
107
- }
108
- return /* @__PURE__ */ jsxs("div", { style: {
109
- marginBottom: "16px",
110
- padding: "16px",
111
- backgroundColor: theme.palette.type === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
112
- borderRadius: "8px",
113
- border: `1px solid ${theme.palette.type === "dark" ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.12)"}`
114
- }, children: [
115
- /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", style: {
116
- marginBottom: "12px",
117
- fontWeight: "bold",
118
- color: theme.palette.text.secondary,
119
- textTransform: "uppercase",
120
- letterSpacing: "0.5px"
121
- }, children: "Metadata" }),
122
- filteredEntries.map(([key, value], index) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: index < filteredEntries.length - 1 ? "12px" : "0" }, children: [
123
- /* @__PURE__ */ jsxs(Typography, { variant: "body2", style: {
124
- fontWeight: "bold",
125
- textTransform: "capitalize",
126
- color: theme.palette.primary.main,
127
- marginBottom: "4px"
128
- }, children: [
129
- key,
130
- ":"
131
- ] }),
132
- /* @__PURE__ */ jsx(Typography, { variant: "body2", style: {
133
- lineHeight: "1.5",
134
- marginLeft: "8px",
135
- color: theme.palette.text.primary
136
- }, children: Array.isArray(value) ? value.join(", ") : String(value) })
137
- ] }, key))
138
- ] });
139
- };
140
- const parseCursorContent = (content) => {
141
- return manualParseFrontmatter(content);
142
- };
141
+ const parseCursorContent = (content) => manualParseFrontmatter(content);
143
142
  const manualParseFrontmatter = (content) => {
144
143
  if (!content.trim().startsWith("---")) {
145
- return {
146
- frontmatter: void 0,
147
- content
148
- };
144
+ return { frontmatter: void 0, content };
149
145
  }
150
146
  try {
151
147
  const lines = content.split("\n");
@@ -156,12 +152,7 @@ const manualParseFrontmatter = (content) => {
156
152
  break;
157
153
  }
158
154
  }
159
- if (frontmatterEndIndex === -1) {
160
- return {
161
- frontmatter: void 0,
162
- content
163
- };
164
- }
155
+ if (frontmatterEndIndex === -1) return { frontmatter: void 0, content };
165
156
  const frontmatterLines = lines.slice(1, frontmatterEndIndex);
166
157
  const contentLines = lines.slice(frontmatterEndIndex + 1);
167
158
  const frontmatter = {};
@@ -178,143 +169,207 @@ const manualParseFrontmatter = (content) => {
178
169
  frontmatter: Object.keys(frontmatter).length > 0 ? frontmatter : void 0,
179
170
  content: contentLines.join("\n").trim()
180
171
  };
181
- } catch (error) {
182
- return {
183
- frontmatter: void 0,
184
- content
185
- };
172
+ } catch (_e) {
173
+ return { frontmatter: void 0, content };
186
174
  }
187
175
  };
188
176
  const constructFileUrl = (gitUrl, filePath) => {
189
177
  const cleanGitUrl = gitUrl.replace(/\/+$/, "");
190
- if (cleanGitUrl.includes("github.com")) {
191
- return `${cleanGitUrl}/blob/main/${filePath}`;
192
- }
193
- if (cleanGitUrl.includes("gitlab.com")) {
194
- return `${cleanGitUrl}/-/blob/main/${filePath}`;
195
- }
178
+ if (cleanGitUrl.includes("github.com")) return `${cleanGitUrl}/blob/main/${filePath}`;
179
+ if (cleanGitUrl.includes("gitlab.com")) return `${cleanGitUrl}/-/blob/main/${filePath}`;
196
180
  return `${cleanGitUrl}/blob/main/${filePath}`;
197
181
  };
182
+ const RuleTypeIcon = ({ type }) => /* @__PURE__ */ jsx(CodeIcon, { style: { color: RULE_TYPE_COLORS[type] ?? "#888", flexShrink: 0 } });
183
+ const renderFrontmatter = (theme, frontmatter) => {
184
+ if (!frontmatter || Object.keys(frontmatter).length === 0) return null;
185
+ const filteredEntries = Object.entries(frontmatter).filter(
186
+ ([key]) => !["description", "globs"].includes(key)
187
+ );
188
+ if (filteredEntries.length === 0) return null;
189
+ return /* @__PURE__ */ jsxs("div", { style: {
190
+ marginBottom: 16,
191
+ padding: 16,
192
+ backgroundColor: theme.palette.type === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
193
+ borderRadius: 8,
194
+ border: `1px solid ${theme.palette.type === "dark" ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.12)"}`
195
+ }, children: [
196
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", style: { marginBottom: 12, fontWeight: "bold", color: theme.palette.text.secondary, textTransform: "uppercase", letterSpacing: "0.5px" }, children: "Metadata" }),
197
+ filteredEntries.map(([key, value], index) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: index < filteredEntries.length - 1 ? 12 : 0 }, children: [
198
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", style: { fontWeight: "bold", textTransform: "capitalize", color: theme.palette.primary.main, marginBottom: 4 }, children: [
199
+ key,
200
+ ":"
201
+ ] }),
202
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", style: { lineHeight: "1.5", marginLeft: 8, color: theme.palette.text.primary }, children: Array.isArray(value) ? value.join(", ") : String(value) })
203
+ ] }, key))
204
+ ] });
205
+ };
206
+ const RuleContentViewer = ({ content }) => {
207
+ const styles = useStyles();
208
+ const [view, setView] = useState("rendered");
209
+ return /* @__PURE__ */ jsxs("div", { children: [
210
+ /* @__PURE__ */ jsxs(
211
+ ToggleButtonGroup,
212
+ {
213
+ size: "small",
214
+ value: view,
215
+ exclusive: true,
216
+ onChange: (_e, v) => {
217
+ if (v) setView(v);
218
+ },
219
+ className: styles.viewToggle,
220
+ children: [
221
+ /* @__PURE__ */ jsx(ToggleButton, { value: "rendered", children: "Rendered" }),
222
+ /* @__PURE__ */ jsx(ToggleButton, { value: "raw", children: "Raw" })
223
+ ]
224
+ }
225
+ ),
226
+ view === "rendered" ? /* @__PURE__ */ jsx("div", { className: styles.ruleContent, children: /* @__PURE__ */ jsx(MarkdownContent, { content }) }) : /* @__PURE__ */ jsx(CodeSnippet, { text: content, language: "markdown" })
227
+ ] });
228
+ };
229
+ const CopyButton = ({ content }) => {
230
+ const [open, setOpen] = useState(false);
231
+ const handleCopy = useCallback((e) => {
232
+ e.stopPropagation();
233
+ navigator.clipboard.writeText(content).then(() => setOpen(true));
234
+ }, [content]);
235
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
236
+ /* @__PURE__ */ jsx(Tooltip, { title: "Copy content", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: handleCopy, children: /* @__PURE__ */ jsx(FileCopyIcon, { fontSize: "small" }) }) }),
237
+ /* @__PURE__ */ jsx(
238
+ Snackbar,
239
+ {
240
+ open,
241
+ autoHideDuration: 2e3,
242
+ onClose: () => setOpen(false),
243
+ message: "Copied to clipboard",
244
+ anchorOrigin: { vertical: "bottom", horizontal: "center" }
245
+ }
246
+ )
247
+ ] });
248
+ };
249
+ const GenericRuleAccordion = ({
250
+ rule,
251
+ label
252
+ }) => {
253
+ const styles = useStyles();
254
+ const theme = useTheme();
255
+ return /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
256
+ /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
257
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
258
+ /* @__PURE__ */ jsx(RuleTypeIcon, { type: rule.type }),
259
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: rule.title || rule.fileName }),
260
+ /* @__PURE__ */ jsx(Chip, { label: label ?? rule.type, size: "small", className: styles.ruleType }),
261
+ rule.mode && /* @__PURE__ */ jsx(Chip, { label: `Mode: ${rule.mode}`, size: "small", variant: "outlined" }),
262
+ rule.alwaysApply !== void 0 && /* @__PURE__ */ jsx(Chip, { label: rule.alwaysApply ? "Always Apply" : "On Demand", size: "small", variant: "outlined" }),
263
+ rule.applyTo && /* @__PURE__ */ jsx(Chip, { label: `Applies to: ${rule.applyTo}`, size: "small", variant: "outlined" })
264
+ ] }),
265
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderActions, children: [
266
+ /* @__PURE__ */ jsx(CopyButton, { content: rule.content }),
267
+ rule.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: (e) => {
268
+ e.stopPropagation();
269
+ window.open(constructFileUrl(rule.gitUrl, rule.filePath), "_blank");
270
+ }, children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" }) }) })
271
+ ] })
272
+ ] }) }),
273
+ /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
274
+ /* @__PURE__ */ jsx("div", { className: styles.ruleMetadata, children: /* @__PURE__ */ jsx(Chip, { label: `Path: ${rule.filePath}`, size: "small", variant: "outlined" }) }),
275
+ rule.frontmatter && renderFrontmatter(theme, rule.frontmatter),
276
+ /* @__PURE__ */ jsx(RuleContentViewer, { content: rule.content })
277
+ ] }) })
278
+ ] });
279
+ };
198
280
  const RuleComponent = ({ rule }) => {
199
281
  const styles = useStyles();
200
282
  const theme = useTheme();
201
- const renderCursorRule = (rule2) => {
202
- const { frontmatter, content } = parseCursorContent(rule2.content);
283
+ const renderCursorRule = (r) => {
284
+ const { frontmatter, content } = parseCursorContent(r.content);
203
285
  return /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
204
286
  /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
205
287
  /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
206
- /* @__PURE__ */ jsx(RuleTypeIcon, { type: rule2.type }),
207
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: rule2.fileName }),
208
- /* @__PURE__ */ jsx(Chip, { label: rule2.type, size: "small", className: styles.ruleType }),
288
+ /* @__PURE__ */ jsx(RuleTypeIcon, { type: r.type }),
289
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: r.fileName }),
290
+ /* @__PURE__ */ jsx(Chip, { label: r.type, size: "small", className: styles.ruleType }),
209
291
  frontmatter?.description && /* @__PURE__ */ jsx(Typography, { variant: "body2", style: { marginLeft: 8, color: theme.palette.text.secondary }, children: frontmatter.description })
210
292
  ] }),
211
- rule2.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(
212
- IconButton,
213
- {
214
- size: "small",
215
- onClick: (e) => {
216
- e.stopPropagation();
217
- window.open(constructFileUrl(rule2.gitUrl, rule2.filePath), "_blank");
218
- },
219
- children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" })
220
- }
221
- ) })
293
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderActions, children: [
294
+ /* @__PURE__ */ jsx(CopyButton, { content }),
295
+ r.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: (e) => {
296
+ e.stopPropagation();
297
+ window.open(constructFileUrl(r.gitUrl, r.filePath), "_blank");
298
+ }, children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" }) }) })
299
+ ] })
222
300
  ] }) }),
223
301
  /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
224
302
  /* @__PURE__ */ jsxs("div", { className: styles.ruleMetadata, children: [
225
- /* @__PURE__ */ jsx(Chip, { label: `Path: ${rule2.filePath}`, size: "small", variant: "outlined" }),
303
+ /* @__PURE__ */ jsx(Chip, { label: `Path: ${r.filePath}`, size: "small", variant: "outlined" }),
226
304
  frontmatter?.globs && /* @__PURE__ */ jsx(Chip, { label: `Globs: ${Array.isArray(frontmatter.globs) ? frontmatter.globs.join(", ") : frontmatter.globs}`, size: "small", variant: "outlined" })
227
305
  ] }),
228
306
  renderFrontmatter(theme, frontmatter),
229
- /* @__PURE__ */ jsx("div", { className: styles.ruleContent, children: /* @__PURE__ */ jsx(MarkdownContent, { content }) })
230
- ] }) })
231
- ] });
232
- };
233
- const renderCopilotRule = (rule2) => {
234
- const ruleNumber = rule2.order || (rule2.fileName.match(/Rule (\d+)/) || [])[1] || rule2.id.split("-").pop();
235
- return /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
236
- /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
237
- /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
238
- /* @__PURE__ */ jsx(RuleTypeIcon, { type: rule2.type }),
239
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: rule2.title || `Copilot Rule #${ruleNumber}` }),
240
- /* @__PURE__ */ jsx(Chip, { label: rule2.type, size: "small", className: styles.ruleType }),
241
- rule2.applyTo && /* @__PURE__ */ jsx(
242
- Chip,
243
- {
244
- label: `Applies to: ${rule2.applyTo}`,
245
- size: "small",
246
- variant: "outlined",
247
- style: { marginLeft: 8 }
248
- }
249
- )
250
- ] }),
251
- rule2.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(
252
- IconButton,
253
- {
254
- size: "small",
255
- onClick: (e) => {
256
- e.stopPropagation();
257
- window.open(constructFileUrl(rule2.gitUrl, rule2.filePath), "_blank");
258
- },
259
- children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" })
260
- }
261
- ) })
262
- ] }) }),
263
- /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
264
- /* @__PURE__ */ jsxs("div", { className: styles.ruleMetadata, children: [
265
- /* @__PURE__ */ jsx(Chip, { label: `Path: ${rule2.filePath}`, size: "small", variant: "outlined" }),
266
- rule2.frontmatter && renderFrontmatter(theme, rule2.frontmatter)
267
- ] }),
268
- /* @__PURE__ */ jsx("div", { className: styles.ruleContent, children: /* @__PURE__ */ jsx(MarkdownContent, { content: rule2.content }) })
307
+ /* @__PURE__ */ jsx(RuleContentViewer, { content })
269
308
  ] }) })
270
309
  ] });
271
310
  };
272
- const renderClineRule = (rule2) => /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
311
+ const renderCopilotRule = (r) => /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
273
312
  /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
274
313
  /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
275
- /* @__PURE__ */ jsx(RuleTypeIcon, { type: rule2.type }),
276
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: rule2.title || rule2.fileName }),
277
- /* @__PURE__ */ jsx(Chip, { label: rule2.type, size: "small", className: styles.ruleType })
314
+ /* @__PURE__ */ jsx(RuleTypeIcon, { type: r.type }),
315
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: r.title || r.fileName }),
316
+ /* @__PURE__ */ jsx(Chip, { label: r.type, size: "small", className: styles.ruleType }),
317
+ r.applyTo && /* @__PURE__ */ jsx(Chip, { label: `Applies to: ${r.applyTo}`, size: "small", variant: "outlined", style: { marginLeft: 8 } })
278
318
  ] }),
279
- rule2.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(
280
- IconButton,
281
- {
282
- size: "small",
283
- onClick: (e) => {
284
- e.stopPropagation();
285
- window.open(constructFileUrl(rule2.gitUrl, rule2.filePath), "_blank");
286
- },
287
- children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" })
288
- }
289
- ) })
319
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderActions, children: [
320
+ /* @__PURE__ */ jsx(CopyButton, { content: r.content }),
321
+ r.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: (e) => {
322
+ e.stopPropagation();
323
+ window.open(constructFileUrl(r.gitUrl, r.filePath), "_blank");
324
+ }, children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" }) }) })
325
+ ] })
290
326
  ] }) }),
291
327
  /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
292
- /* @__PURE__ */ jsx("div", { className: styles.ruleMetadata, children: /* @__PURE__ */ jsx(Chip, { label: `Path: ${rule2.filePath}`, size: "small", variant: "outlined" }) }),
293
- /* @__PURE__ */ jsx("div", { className: styles.ruleContent, children: /* @__PURE__ */ jsx(MarkdownContent, { content: rule2.content }) })
328
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleMetadata, children: [
329
+ /* @__PURE__ */ jsx(Chip, { label: `Path: ${r.filePath}`, size: "small", variant: "outlined" }),
330
+ r.frontmatter && renderFrontmatter(theme, r.frontmatter)
331
+ ] }),
332
+ /* @__PURE__ */ jsx(RuleContentViewer, { content: r.content })
294
333
  ] }) })
295
334
  ] });
296
- const renderClaudeCodeRule = (rule2) => /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
335
+ const renderClineRule = (r) => /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
297
336
  /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
298
337
  /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
299
- /* @__PURE__ */ jsx(RuleTypeIcon, { type: rule2.type }),
300
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: rule2.title || rule2.fileName }),
301
- /* @__PURE__ */ jsx(Chip, { label: rule2.type, size: "small", className: styles.ruleType })
338
+ /* @__PURE__ */ jsx(RuleTypeIcon, { type: r.type }),
339
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: r.title || r.fileName }),
340
+ /* @__PURE__ */ jsx(Chip, { label: r.type, size: "small", className: styles.ruleType })
302
341
  ] }),
303
- rule2.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(
304
- IconButton,
305
- {
306
- size: "small",
307
- onClick: (e) => {
308
- e.stopPropagation();
309
- window.open(constructFileUrl(rule2.gitUrl, rule2.filePath), "_blank");
310
- },
311
- children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" })
312
- }
313
- ) })
342
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderActions, children: [
343
+ /* @__PURE__ */ jsx(CopyButton, { content: r.content }),
344
+ r.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: (e) => {
345
+ e.stopPropagation();
346
+ window.open(constructFileUrl(r.gitUrl, r.filePath), "_blank");
347
+ }, children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" }) }) })
348
+ ] })
314
349
  ] }) }),
315
350
  /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
316
- /* @__PURE__ */ jsx("div", { className: styles.ruleMetadata, children: /* @__PURE__ */ jsx(Chip, { label: `Path: ${rule2.filePath}`, size: "small", variant: "outlined" }) }),
317
- /* @__PURE__ */ jsx("div", { className: styles.ruleContent, children: /* @__PURE__ */ jsx(MarkdownContent, { content: rule2.content }) })
351
+ /* @__PURE__ */ jsx("div", { className: styles.ruleMetadata, children: /* @__PURE__ */ jsx(Chip, { label: `Path: ${r.filePath}`, size: "small", variant: "outlined" }) }),
352
+ /* @__PURE__ */ jsx(RuleContentViewer, { content: r.content })
353
+ ] }) })
354
+ ] });
355
+ const renderClaudeCodeRule = (r) => /* @__PURE__ */ jsxs(Accordion, { className: styles.ruleCard, children: [
356
+ /* @__PURE__ */ jsx(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsxs("div", { className: styles.ruleHeader, children: [
357
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderContent, children: [
358
+ /* @__PURE__ */ jsx(RuleTypeIcon, { type: r.type }),
359
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: r.title || r.fileName }),
360
+ /* @__PURE__ */ jsx(Chip, { label: "claude-code", size: "small", className: styles.ruleType })
361
+ ] }),
362
+ /* @__PURE__ */ jsxs("div", { className: styles.ruleHeaderActions, children: [
363
+ /* @__PURE__ */ jsx(CopyButton, { content: r.content }),
364
+ r.gitUrl && /* @__PURE__ */ jsx(Tooltip, { title: "Open file in repository", children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: (e) => {
365
+ e.stopPropagation();
366
+ window.open(constructFileUrl(r.gitUrl, r.filePath), "_blank");
367
+ }, children: /* @__PURE__ */ jsx(LaunchIcon, { fontSize: "small" }) }) })
368
+ ] })
369
+ ] }) }),
370
+ /* @__PURE__ */ jsx(AccordionDetails, { children: /* @__PURE__ */ jsxs("div", { children: [
371
+ /* @__PURE__ */ jsx("div", { className: styles.ruleMetadata, children: /* @__PURE__ */ jsx(Chip, { label: `Path: ${r.filePath}`, size: "small", variant: "outlined" }) }),
372
+ /* @__PURE__ */ jsx(RuleContentViewer, { content: r.content })
318
373
  ] }) })
319
374
  ] });
320
375
  switch (rule.type) {
@@ -327,51 +382,73 @@ const RuleComponent = ({ rule }) => {
327
382
  case AIRuleType.CLINE:
328
383
  return renderClineRule(rule);
329
384
  default:
330
- return null;
385
+ return /* @__PURE__ */ jsx(GenericRuleAccordion, { rule });
331
386
  }
332
387
  };
388
+ const exportRulesToMarkdown = (rules) => {
389
+ const lines = ["# AI Coding Rules Export\n"];
390
+ const grouped = {};
391
+ for (const rule of rules) {
392
+ if (!grouped[rule.type]) grouped[rule.type] = [];
393
+ grouped[rule.type].push(rule);
394
+ }
395
+ for (const type of RULE_TYPE_DISPLAY_ORDER) {
396
+ const typeRules = grouped[type];
397
+ if (!typeRules || typeRules.length === 0) continue;
398
+ lines.push(`## ${RULE_TYPE_DISPLAY_NAMES[type]}
399
+ `);
400
+ for (const rule of typeRules) {
401
+ const title = rule.title || rule.fileName;
402
+ lines.push(`### ${title}
403
+ `);
404
+ lines.push(`_File: \`${rule.filePath}\`_
405
+ `);
406
+ lines.push(`${rule.content}
407
+ `);
408
+ }
409
+ }
410
+ const blob = new Blob([lines.join("\n")], { type: "text/markdown" });
411
+ const url = URL.createObjectURL(blob);
412
+ const a = document.createElement("a");
413
+ a.href = url;
414
+ a.download = "ai-rules-export.md";
415
+ a.click();
416
+ URL.revokeObjectURL(url);
417
+ };
333
418
  const AIRulesComponent = ({ title = "AI Coding Rules" } = {}) => {
334
- const { rulesByType, loading, error, hasGitUrl, totalRules, allowedRuleTypes, selectedRuleTypes, setSelectedRuleTypes, applyFilters, resetFilters, hasUnappliedChanges, hasSearched } = useAiRules();
419
+ const { rulesByType, rules, loading, error, hasGitUrl, totalRules, allowedRuleTypes, selectedRuleTypes, setSelectedRuleTypes, applyFilters, resetFilters, hasUnappliedChanges, hasSearched } = useAiRules();
335
420
  const styles = useStyles();
336
- const ruleTypeDisplayOrder = [AIRuleType.CURSOR, AIRuleType.CLAUDE_CODE, AIRuleType.COPILOT, AIRuleType.CLINE];
337
- const formatRuleTypeName = (type) => {
338
- switch (type) {
339
- case AIRuleType.CURSOR:
340
- return "Cursor";
341
- case AIRuleType.CLAUDE_CODE:
342
- return "Claude Code";
343
- case AIRuleType.COPILOT:
344
- return "Copilot";
345
- case AIRuleType.CLINE:
346
- return "Cline";
347
- }
348
- };
421
+ const [searchQuery, setSearchQuery] = useState("");
422
+ const formatRuleTypeName = (type) => RULE_TYPE_DISPLAY_NAMES[type] ?? type;
349
423
  const handleTypeToggle = (type, checked) => {
350
424
  const newTypes = checked ? [...selectedRuleTypes, type] : selectedRuleTypes.filter((t) => t !== type);
351
425
  setSelectedRuleTypes(newTypes);
352
426
  };
427
+ const filteredRulesByType = useMemo(() => {
428
+ if (!searchQuery.trim()) return rulesByType;
429
+ const q = searchQuery.toLowerCase();
430
+ const filtered = {};
431
+ for (const type of RULE_TYPE_DISPLAY_ORDER) {
432
+ const typeRules = (rulesByType[type] || []).filter((rule) => {
433
+ const r = rule;
434
+ return rule.content?.toLowerCase().includes(q) || rule.fileName?.toLowerCase().includes(q) || r.title?.toLowerCase().includes(q) || r.description?.toLowerCase().includes(q);
435
+ });
436
+ if (typeRules.length > 0) filtered[type] = typeRules;
437
+ }
438
+ return filtered;
439
+ }, [rulesByType, searchQuery]);
440
+ const filteredTotal = useMemo(
441
+ () => Object.values(filteredRulesByType).reduce((sum, arr) => sum + (arr?.length ?? 0), 0),
442
+ [filteredRulesByType]
443
+ );
353
444
  if (loading) {
354
445
  return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(Progress, {}) });
355
446
  }
356
447
  if (!hasGitUrl) {
357
- return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(
358
- EmptyState,
359
- {
360
- missing: "content",
361
- title: "No Git Repository",
362
- description: "This component doesn't have a Git source URL configured."
363
- }
364
- ) });
448
+ return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(EmptyState, { missing: "content", title: "No Git Repository", description: "This component doesn't have a Git source URL configured." }) });
365
449
  }
366
450
  if (error) {
367
- return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(
368
- EmptyState,
369
- {
370
- missing: "content",
371
- title: "Error Loading Rules",
372
- description: error
373
- }
374
- ) });
451
+ return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(EmptyState, { missing: "content", title: "Error Loading Rules", description: error }) });
375
452
  }
376
453
  return /* @__PURE__ */ jsxs(InfoCard, { title, className: styles.root, children: [
377
454
  /* @__PURE__ */ jsxs("div", { className: styles.filterSection, children: [
@@ -379,28 +456,13 @@ const AIRulesComponent = ({ title = "AI Coding Rules" } = {}) => {
379
456
  /* @__PURE__ */ jsx("div", { className: styles.filterContainer, children: allowedRuleTypes.map((type) => /* @__PURE__ */ jsx(
380
457
  FormControlLabel,
381
458
  {
382
- control: /* @__PURE__ */ jsx(
383
- Checkbox,
384
- {
385
- checked: selectedRuleTypes.includes(type),
386
- onChange: (e) => handleTypeToggle(type, e.target.checked)
387
- }
388
- ),
459
+ control: /* @__PURE__ */ jsx(Checkbox, { checked: selectedRuleTypes.includes(type), onChange: (e) => handleTypeToggle(type, e.target.checked) }),
389
460
  label: formatRuleTypeName(type)
390
461
  },
391
462
  type
392
463
  )) }),
393
464
  /* @__PURE__ */ jsxs("div", { className: styles.applyFilterButton, children: [
394
- /* @__PURE__ */ jsx(
395
- Button,
396
- {
397
- variant: "contained",
398
- color: "primary",
399
- onClick: applyFilters,
400
- disabled: !hasUnappliedChanges,
401
- children: "Apply Filter"
402
- }
403
- ),
465
+ /* @__PURE__ */ jsx(Button, { variant: "contained", color: "primary", onClick: applyFilters, disabled: !hasUnappliedChanges, children: "Apply Filter" }),
404
466
  hasUnappliedChanges && /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", style: { marginTop: 8 }, children: 'You have unsaved filter changes. Click "Apply Filter" to update the results.' }),
405
467
  !hasUnappliedChanges && selectedRuleTypes.length === 0 && /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", style: { marginTop: 8 }, children: "Select at least one rule type to search for AI rules." })
406
468
  ] })
@@ -411,32 +473,48 @@ const AIRulesComponent = ({ title = "AI Coding Rules" } = {}) => {
411
473
  missing: "content",
412
474
  title: "No AI Rules Found",
413
475
  description: "No AI rules were found in this repository for the selected rule types.",
414
- action: /* @__PURE__ */ jsx(
415
- Button,
416
- {
417
- variant: "outlined",
418
- onClick: resetFilters,
419
- children: "Reset Filters"
420
- }
421
- )
476
+ action: /* @__PURE__ */ jsx(Button, { variant: "outlined", onClick: resetFilters, children: "Reset Filters" })
422
477
  }
423
478
  ) : totalRules > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
479
+ /* @__PURE__ */ jsx(
480
+ TextField,
481
+ {
482
+ className: styles.searchBar,
483
+ variant: "outlined",
484
+ size: "small",
485
+ label: "Search rules",
486
+ placeholder: "Search by name, title, or content\u2026",
487
+ value: searchQuery,
488
+ onChange: (e) => setSearchQuery(e.target.value)
489
+ }
490
+ ),
424
491
  /* @__PURE__ */ jsxs("div", { className: styles.statsContainer, children: [
425
492
  /* @__PURE__ */ jsx(Card, { className: styles.statCard, children: /* @__PURE__ */ jsxs(CardContent, { children: [
426
- /* @__PURE__ */ jsx(Typography, { variant: "h4", children: totalRules }),
427
- /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "Total Rules" })
493
+ /* @__PURE__ */ jsx(Typography, { variant: "h4", children: searchQuery ? filteredTotal : totalRules }),
494
+ /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: searchQuery ? "Matching" : "Total Rules" })
428
495
  ] }) }),
429
- ruleTypeDisplayOrder.map((type) => {
430
- const typeRules = rulesByType[type] || [];
496
+ RULE_TYPE_DISPLAY_ORDER.map((type) => {
497
+ const typeRules = (searchQuery ? filteredRulesByType : rulesByType)[type] || [];
431
498
  if (typeRules.length === 0) return null;
432
499
  return /* @__PURE__ */ jsx(Card, { className: styles.statCard, children: /* @__PURE__ */ jsxs(CardContent, { children: [
433
500
  /* @__PURE__ */ jsx(Typography, { variant: "h4", children: typeRules.length }),
434
501
  /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: formatRuleTypeName(type) })
435
502
  ] }) }, type);
436
- })
503
+ }),
504
+ /* @__PURE__ */ jsx(Tooltip, { title: "Download all rules as Markdown", children: /* @__PURE__ */ jsx(
505
+ Button,
506
+ {
507
+ variant: "outlined",
508
+ size: "small",
509
+ startIcon: /* @__PURE__ */ jsx(GetAppIcon, {}),
510
+ className: styles.exportButton,
511
+ onClick: () => exportRulesToMarkdown(rules),
512
+ children: "Export"
513
+ }
514
+ ) })
437
515
  ] }),
438
- ruleTypeDisplayOrder.map((type) => {
439
- const typeRules = rulesByType[type] || [];
516
+ RULE_TYPE_DISPLAY_ORDER.map((type) => {
517
+ const typeRules = (searchQuery ? filteredRulesByType : rulesByType)[type] || [];
440
518
  if (typeRules.length === 0) return null;
441
519
  return /* @__PURE__ */ jsxs("div", { children: [
442
520
  /* @__PURE__ */ jsxs(Typography, { variant: "h5", gutterBottom: true, style: { marginTop: 16 }, children: [
@@ -447,7 +525,8 @@ const AIRulesComponent = ({ title = "AI Coding Rules" } = {}) => {
447
525
  ] }),
448
526
  typeRules.map((rule) => /* @__PURE__ */ jsx(RuleComponent, { rule }, rule.id))
449
527
  ] }, type);
450
- })
528
+ }),
529
+ searchQuery && filteredTotal === 0 && /* @__PURE__ */ jsx(EmptyState, { missing: "content", title: "No matching rules", description: `No rules match "${searchQuery}". Clear the search to show all rules.` })
451
530
  ] }) : /* @__PURE__ */ jsx("div", { style: { marginTop: 16 }, children: /* @__PURE__ */ jsx(Typography, { variant: "body1", color: "textSecondary", children: 'Select rule types above and click "Apply Filter" to search for AI coding rules in this repository.' }) })
452
531
  ] });
453
532
  };