From cb1400dce8a99cff8b226b71f6cde39402fa781f Mon Sep 17 00:00:00 2001 From: Daniel Klys Date: Mon, 12 May 2025 02:24:07 +0200 Subject: [PATCH 1/3] feat: add support for github split instructions --- src/components/rule-preview/RulePreview.tsx | 21 ++++++- .../rule-preview/RulePreviewTopbar.tsx | 41 ++++++++++++- src/components/rule-preview/RulesPath.tsx | 15 ++++- .../RulesPreviewCopyDownloadActions.tsx | 4 +- .../settings-preview/SettingsPreview.tsx | 57 +++++++++++++++++++ src/data/ai-environments.ts | 2 + .../rules-builder/RulesBuilderService.ts | 20 ++++++- .../rules-builder/RulesBuilderTypes.ts | 8 ++- src/store/projectStore.ts | 16 +++++- 9 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 src/components/settings-preview/SettingsPreview.tsx diff --git a/src/components/rule-preview/RulePreview.tsx b/src/components/rule-preview/RulePreview.tsx index 13c9017..eb8e744 100644 --- a/src/components/rule-preview/RulePreview.tsx +++ b/src/components/rule-preview/RulePreview.tsx @@ -1,21 +1,27 @@ import React, { useCallback, useEffect, useState } from 'react'; import { RulesBuilderService } from '../../services/rules-builder/RulesBuilderService.ts'; -import { useProjectStore } from '../../store/projectStore'; +import { useProjectStore, adaptableFileEnvironments } from '../../store/projectStore'; import { useTechStackStore } from '../../store/techStackStore'; import { useDependencyUpload } from '../rule-parser/useDependencyUpload'; import { RulePreviewTopbar } from './RulePreviewTopbar'; import { DependencyUpload } from './DependencyUpload.tsx'; import { MarkdownContentRenderer } from './MarkdownContentRenderer.tsx'; import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts'; +import { JsonContentRenderer } from '../settings-preview/SettingsPreview.tsx'; export const RulePreview: React.FC = () => { const { selectedLibraries } = useTechStackStore(); - const { projectName, projectDescription, isMultiFileEnvironment } = useProjectStore(); + const { projectName, projectDescription, isMultiFileEnvironment, selectedEnvironment } = + useProjectStore(); const [markdownContent, setMarkdownContent] = useState([]); + const [settingsContent, setSettingsContent] = useState([]); const [isDragging, setIsDragging] = useState(false); const { uploadStatus, uploadDependencyFile } = useDependencyUpload(); useEffect(() => { + const shouldDisplaySettings = + adaptableFileEnvironments.has(selectedEnvironment) && isMultiFileEnvironment; + const settings = RulesBuilderService.generateSettingsContent(selectedLibraries); const markdowns = RulesBuilderService.generateRulesContent( projectName, projectDescription, @@ -23,7 +29,14 @@ export const RulePreview: React.FC = () => { isMultiFileEnvironment, ); setMarkdownContent(markdowns); - }, [selectedLibraries, projectName, projectDescription, isMultiFileEnvironment]); + setSettingsContent(shouldDisplaySettings ? settings : []); + }, [ + selectedLibraries, + projectName, + projectDescription, + isMultiFileEnvironment, + selectedEnvironment, + ]); // Handle drag events const handleDragOver = useCallback((e: React.DragEvent) => { @@ -68,6 +81,8 @@ export const RulePreview: React.FC = () => { {/* Dropzone overlay */} + {/* Settings content */} + {/* Markdown content */} diff --git a/src/components/rule-preview/RulePreviewTopbar.tsx b/src/components/rule-preview/RulePreviewTopbar.tsx index 2ff1b8d..fe5adbd 100644 --- a/src/components/rule-preview/RulePreviewTopbar.tsx +++ b/src/components/rule-preview/RulePreviewTopbar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useProjectStore } from '../../store/projectStore'; +import { useProjectStore, adaptableFileEnvironments } from '../../store/projectStore'; import { RulesPath } from './RulesPath'; import { RulesPreviewActions } from './RulesPreviewActions'; import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts'; @@ -37,8 +37,13 @@ const EnvButton: React.FC = ({ }; export const RulePreviewTopbar: React.FC = ({ rulesContent }) => { - const { selectedEnvironment, setSelectedEnvironment, isMultiFileEnvironment, isHydrated } = - useProjectStore(); + const { + selectedEnvironment, + setSelectedEnvironment, + isMultiFileEnvironment, + isHydrated, + setMultiFileEnvironment, + } = useProjectStore(); // If state hasn't been hydrated from storage yet, don't render the selector // This prevents the "blinking" effect when loading persisted state @@ -85,6 +90,36 @@ export const RulePreviewTopbar: React.FC = ({ rulesConte {/* Path display */} + + {adaptableFileEnvironments.has(selectedEnvironment) && ( +
+
+ setMultiFileEnvironment(e.target.checked)} + aria-describedby="multi-file-description" + className="w-4 h-4 rounded border-gray-600 bg-gray-700 text-indigo-500 focus:ring-indigo-500 focus:ring-2 focus:ring-offset-gray-800" + /> + +
+ + When enabled, instructions will be split into separate domain-specific files + +
+ )} {/* Right side: Action buttons */} diff --git a/src/components/rule-preview/RulesPath.tsx b/src/components/rule-preview/RulesPath.tsx index 74fb524..3a0efd3 100644 --- a/src/components/rule-preview/RulesPath.tsx +++ b/src/components/rule-preview/RulesPath.tsx @@ -1,12 +1,21 @@ import React from 'react'; -import { useProjectStore } from '../../store/projectStore'; +import { adaptableFileEnvironments, useProjectStore } from '../../store/projectStore'; import { aiEnvironmentConfig } from '../../data/ai-environments.ts'; export const RulesPath: React.FC = () => { - const { selectedEnvironment } = useProjectStore(); + const { selectedEnvironment, isMultiFileEnvironment } = useProjectStore(); // Get the appropriate file path based on the selected format - const getFilePath = (): string => aiEnvironmentConfig[selectedEnvironment].filePath; + const shouldUseAlternativePath = + isMultiFileEnvironment && adaptableFileEnvironments.has(selectedEnvironment); + + const getFilePath = (): string => { + const config = aiEnvironmentConfig[selectedEnvironment]; + + return shouldUseAlternativePath && config.alternativeFilePath + ? config.alternativeFilePath + : config.filePath; + }; return (
diff --git a/src/components/rule-preview/RulesPreviewCopyDownloadActions.tsx b/src/components/rule-preview/RulesPreviewCopyDownloadActions.tsx index 59fd9e4..3c9508b 100644 --- a/src/components/rule-preview/RulesPreviewCopyDownloadActions.tsx +++ b/src/components/rule-preview/RulesPreviewCopyDownloadActions.tsx @@ -6,10 +6,12 @@ import { useProjectStore } from '../../store/projectStore'; interface RulesPreviewCopyDownloadActionsProps { rulesContent: RulesContent[]; + filePath?: string; } export const RulesPreviewCopyDownloadActions: React.FC = ({ rulesContent, + filePath, }) => { const { selectedEnvironment, isMultiFileEnvironment } = useProjectStore(); const [showCopiedMessage, setShowCopiedMessage] = useState(false); @@ -85,7 +87,7 @@ export const RulesPreviewCopyDownloadActions: React.FC { + try { + // Parse and re-stringify for proper formatting + const parsed = JSON.parse(jsonContent); + const formatted = JSON.stringify(parsed, null, 2); + + // Basic syntax highlighting + return ( + + {formatted.split('\n').map((line, i) => { + // Highlight keys in quotes + const highlightedLine = line.replace( + /"([^"]+)":/g, + '"$1":', + ); + + return
; + })} + + ); + } catch (e) { + // Return error message if JSON parsing fails + return Invalid JSON: {String(e)}; + } +}; + +// Component for rendering JSON content +export const JsonContentRenderer: React.FC<{ jsonContent: RulesContent[] }> = ({ jsonContent }) => { + return ( +
+ {jsonContent.map((rule, index) => ( +
+
+ +
+ +
+            {formatJsonWithSyntaxHighlighting(rule.markdown)}
+          
+
+ ))} +
+ ); +}; + +export default JsonContentRenderer; diff --git a/src/data/ai-environments.ts b/src/data/ai-environments.ts index db22f43..a7a53aa 100644 --- a/src/data/ai-environments.ts +++ b/src/data/ai-environments.ts @@ -14,6 +14,7 @@ export type AIEnvironment = `${AIEnvironmentName}`; type AIEnvironmentConfig = { [key in AIEnvironmentName]: { filePath: string; + alternativeFilePath?: string; docsUrl: string; }; }; @@ -21,6 +22,7 @@ type AIEnvironmentConfig = { export const aiEnvironmentConfig: AIEnvironmentConfig = { github: { filePath: '.github/copilot-instructions.md', + alternativeFilePath: '.github/{rule}.instructions.md', docsUrl: 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot', }, diff --git a/src/services/rules-builder/RulesBuilderService.ts b/src/services/rules-builder/RulesBuilderService.ts index c2a8378..d244075 100644 --- a/src/services/rules-builder/RulesBuilderService.ts +++ b/src/services/rules-builder/RulesBuilderService.ts @@ -5,7 +5,7 @@ import { getLayerByStack, getStacksByLibrary, } from '../../data/dictionaries.ts'; -import type { RulesContent } from './RulesBuilderTypes.ts'; +import type { RulesContent, SettingsContent } from './RulesBuilderTypes.ts'; import type { RulesGenerationStrategy } from './RulesGenerationStrategy.ts'; import { MultiFileRulesStrategy } from './rules-generation-strategies/MultiFileRulesStrategy.ts'; import { SingleFileRulesStrategy } from './rules-generation-strategies/SingleFileRulesStrategy.ts'; @@ -46,6 +46,24 @@ export class RulesBuilderService { ); } + static generateSettingsContent(selectedLibraries: Library[]): SettingsContent[] { + return [ + { + markdown: JSON.stringify([ + { + 'github.copilot.chat.codeGeneration.instructions': [ + selectedLibraries.map((library) => ({ + path: `.github/${library.toLowerCase()}.instructions.md`, + })), + ], + }, + ]), + label: 'Settings', + fileName: 'settings.json', + }, + ]; + } + /** * Groups libraries by their stack * diff --git a/src/services/rules-builder/RulesBuilderTypes.ts b/src/services/rules-builder/RulesBuilderTypes.ts index 389c055..de71436 100644 --- a/src/services/rules-builder/RulesBuilderTypes.ts +++ b/src/services/rules-builder/RulesBuilderTypes.ts @@ -1,5 +1,11 @@ export interface RulesContent { markdown: string; label: string; - fileName: `${string}.mdc`; + fileName: `${string}`; +} + +export interface SettingsContent { + markdown: string; + label: string; + fileName: `${string}`; } diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts index f4eb514..35fab29 100644 --- a/src/store/projectStore.ts +++ b/src/store/projectStore.ts @@ -17,12 +17,18 @@ interface ProjectState { setProjectDescription: (description: string) => void; setSelectedEnvironment: (environment: AIEnvironment) => void; setHydrated: () => void; + setMultiFileEnvironment: (isMultiFile: boolean) => void; } export const multiFileEnvironments: ReadonlySet = new Set([ AIEnvironmentName.Cline, AIEnvironmentName.Cursor, ]); + +export const adaptableFileEnvironments: ReadonlySet = new Set([ + AIEnvironmentName.GitHub, +]); + export const initialEnvironment: Readonly = AIEnvironmentName.Cursor; // Create a store with persistence @@ -39,12 +45,15 @@ export const useProjectStore = create()( // Actions setProjectName: (name: string) => set({ projectName: name }), setProjectDescription: (description: string) => set({ projectDescription: description }), - setSelectedEnvironment: (environment: AIEnvironment) => - set({ + setSelectedEnvironment: (environment: AIEnvironment) => { + return set({ selectedEnvironment: environment, isMultiFileEnvironment: multiFileEnvironments.has(environment), - }), + }); + }, setHydrated: () => set({ isHydrated: true }), + setMultiFileEnvironment: (isMultiFile: boolean) => + set({ isMultiFileEnvironment: isMultiFile }), }), { name: 'ai-rules-project-storage', @@ -57,6 +66,7 @@ export const useProjectStore = create()( // Set hydration flag when storage is hydrated onRehydrateStorage: () => (state) => { if (state) { + state.setMultiFileEnvironment(multiFileEnvironments.has(state.selectedEnvironment)); state.setHydrated(); } }, From eaad3322c31d8ea65248cb509063034a4bcbc747 Mon Sep 17 00:00:00 2001 From: Daniel Klys Date: Mon, 12 May 2025 03:27:24 +0200 Subject: [PATCH 2/3] add extension support for rules generation strategies --- src/components/rule-preview/RulePreview.tsx | 6 +++++ .../rules-builder/RulesBuilderService.ts | 24 ++++++++++++++++--- .../rules-builder/RulesGenerationStrategy.ts | 1 + .../MultiFileRulesStrategy.ts | 13 ++++++++-- .../SingleFileRulesStrategy.ts | 5 ++-- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/components/rule-preview/RulePreview.tsx b/src/components/rule-preview/RulePreview.tsx index eb8e744..4836505 100644 --- a/src/components/rule-preview/RulePreview.tsx +++ b/src/components/rule-preview/RulePreview.tsx @@ -8,6 +8,7 @@ import { DependencyUpload } from './DependencyUpload.tsx'; import { MarkdownContentRenderer } from './MarkdownContentRenderer.tsx'; import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts'; import { JsonContentRenderer } from '../settings-preview/SettingsPreview.tsx'; +import { AIEnvironmentName } from '@/data/ai-environments.ts'; export const RulePreview: React.FC = () => { const { selectedLibraries } = useTechStackStore(); @@ -21,12 +22,17 @@ export const RulePreview: React.FC = () => { useEffect(() => { const shouldDisplaySettings = adaptableFileEnvironments.has(selectedEnvironment) && isMultiFileEnvironment; + + const extension = + AIEnvironmentName.GitHub && isMultiFileEnvironment ? 'instructions.md' : 'mdc'; + const settings = RulesBuilderService.generateSettingsContent(selectedLibraries); const markdowns = RulesBuilderService.generateRulesContent( projectName, projectDescription, selectedLibraries, isMultiFileEnvironment, + extension, ); setMarkdownContent(markdowns); setSettingsContent(shouldDisplaySettings ? settings : []); diff --git a/src/services/rules-builder/RulesBuilderService.ts b/src/services/rules-builder/RulesBuilderService.ts index d244075..007a5d0 100644 --- a/src/services/rules-builder/RulesBuilderService.ts +++ b/src/services/rules-builder/RulesBuilderService.ts @@ -28,6 +28,7 @@ export class RulesBuilderService { projectDescription: string, selectedLibraries: Library[], multiFile?: boolean, + extension?: string, ): RulesContent[] { // Group libraries by stack and layer const librariesByStack = this.groupLibrariesByStack(selectedLibraries); @@ -43,18 +44,35 @@ export class RulesBuilderService { selectedLibraries, stacksByLayer, librariesByStack, + extension, ); } static generateSettingsContent(selectedLibraries: Library[]): SettingsContent[] { + const strategy = new MultiFileRulesStrategy(); + const librariesByStack = this.groupLibrariesByStack(selectedLibraries); + return [ { markdown: JSON.stringify([ { 'github.copilot.chat.codeGeneration.instructions': [ - selectedLibraries.map((library) => ({ - path: `.github/${library.toLowerCase()}.instructions.md`, - })), + selectedLibraries.map((library) => { + const stackKey = Object.keys(librariesByStack).find((key) => + librariesByStack[key as Stack].includes(library), + ) as Stack | undefined; + + const layer = stackKey ? getLayerByStack(stackKey) : ''; + + const fileName = strategy.createFileName({ + label: `${layer} - ${stackKey} - ${library}`, + extension: 'instructions.md', + }); + + return { + path: `.github/${fileName}`, + }; + }), ], }, ]), diff --git a/src/services/rules-builder/RulesGenerationStrategy.ts b/src/services/rules-builder/RulesGenerationStrategy.ts index c0529ca..47a2501 100644 --- a/src/services/rules-builder/RulesGenerationStrategy.ts +++ b/src/services/rules-builder/RulesGenerationStrategy.ts @@ -11,5 +11,6 @@ export interface RulesGenerationStrategy { selectedLibraries: Library[], stacksByLayer: Record, librariesByStack: Record, + extension?: string, ): RulesContent[]; } diff --git a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts index 124b632..e0dada9 100644 --- a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts @@ -14,11 +14,12 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { selectedLibraries: Library[], stacksByLayer: Record, librariesByStack: Record, + extension = 'mdc', ): RulesContent[] { const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`; const noSelectedLibrariesMarkdown = `---\n\nšŸ‘ˆ Use the Rule Builder on the left or drop dependency file here`; const projectLabel = 'Project', - projectFileName = 'project.mdc'; + projectFileName = `project.${extension}`; const markdowns: RulesContent[] = []; @@ -38,6 +39,7 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { stack, library, libraryRules: getRulesForLibrary(library), + extension, }), ); }); @@ -47,19 +49,26 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { return markdowns; } + createFileName = ({ label, extension = 'mdc' }: { label: string; extension?: string }) => { + const fileName: RulesContent['fileName'] = `${slugify(label)}.${extension}`; + return fileName; + }; + private buildRulesContent({ libraryRules, layer, stack, library, + extension = 'mdc', }: { libraryRules: string[]; layer: string; stack: string; library: string; + extension?: string; }): RulesContent { const label = `${layer} - ${stack} - ${library}`; - const fileName: RulesContent['fileName'] = `${slugify(`${layer}-${stack}-${library}`)}.mdc`; + const fileName = this.createFileName({ label, extension }); const content = libraryRules.length > 0 ? `${libraryRules.map((rule) => `- ${rule}`).join('\n')}` diff --git a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts index 467c9e7..c8885ce 100644 --- a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts @@ -13,11 +13,12 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy { selectedLibraries: Library[], stacksByLayer: Record, librariesByStack: Record, + extension = 'mdc', ): RulesContent[] { const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`; const noSelectedLibrariesMarkdown = `---\n\nšŸ‘ˆ Use the Rule Builder on the left or drop dependency file here`; const projectLabel = 'Project', - projectFileName = 'project.mdc'; + projectFileName = `project.${extension}`; let markdown = projectMarkdown; @@ -27,7 +28,7 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy { } markdown += this.generateLibraryMarkdown(stacksByLayer, librariesByStack); - return [{ markdown, label: 'All Rules', fileName: 'rules.mdc' }]; + return [{ markdown, label: 'All Rules', fileName: `rules.${extension}` }]; } private generateLibraryMarkdown( From 46eb46e3bacb41e42c9ece8620d5961d59257191 Mon Sep 17 00:00:00 2001 From: Daniel Klys Date: Mon, 12 May 2025 03:32:44 +0200 Subject: [PATCH 3/3] feat: update file path key in rules generation response --- src/services/rules-builder/RulesBuilderService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/rules-builder/RulesBuilderService.ts b/src/services/rules-builder/RulesBuilderService.ts index 007a5d0..0cac422 100644 --- a/src/services/rules-builder/RulesBuilderService.ts +++ b/src/services/rules-builder/RulesBuilderService.ts @@ -70,7 +70,7 @@ export class RulesBuilderService { }); return { - path: `.github/${fileName}`, + file: `.github/${fileName}`, }; }), ],