diff --git a/src/core/prompts/instructions/__tests__/generate-rules.test.ts b/src/core/prompts/instructions/__tests__/generate-rules.test.ts index a04e91a8c0..8fdb751ac4 100644 --- a/src/core/prompts/instructions/__tests__/generate-rules.test.ts +++ b/src/core/prompts/instructions/__tests__/generate-rules.test.ts @@ -90,4 +90,65 @@ describe("ruleTypeDefinitions", () => { expect(rule.analysisSteps.length).toBeGreaterThan(0) }) }) + + it("should have correct paths for each rule type", () => { + expect(ruleTypeDefinitions.general.path).toBe(".roo/rules/coding-standards.md") + expect(ruleTypeDefinitions.code.path).toBe(".roo/rules-code/implementation-rules.md") + expect(ruleTypeDefinitions.architect.path).toBe(".roo/rules-architect/architecture-rules.md") + expect(ruleTypeDefinitions.debug.path).toBe(".roo/rules-debug/debugging-rules.md") + expect(ruleTypeDefinitions["docs-extractor"].path).toBe(".roo/rules-docs-extractor/documentation-rules.md") + }) + + it("should include proper instructions for existing rule files", () => { + const ruleInstructions: RuleInstruction[] = [ruleTypeDefinitions.general] + const options: RulesGenerationOptions = { + selectedRuleTypes: ["general"], + addToGitignore: false, + alwaysAllowWriteProtected: false, + includeCustomRules: false, + customRulesText: "", + } + + const result = generateRulesInstructions(ruleInstructions, options) + + expect(result).toContain("Look for existing rule files") + expect(result).toContain("CLAUDE.md, .cursorrules, .cursor/rules, or .github/copilot-instructions.md") + expect(result).toContain("If found, incorporate and improve upon their content") + }) + + it("should include instructions to open files after generation", () => { + const ruleInstructions: RuleInstruction[] = [ruleTypeDefinitions.general] + const options: RulesGenerationOptions = { + selectedRuleTypes: ["general"], + addToGitignore: false, + alwaysAllowWriteProtected: false, + includeCustomRules: false, + customRulesText: "", + } + + const result = generateRulesInstructions(ruleInstructions, options) + + expect(result).toContain("Open the generated files") + expect(result).toContain("in the editor for review after creation") + }) + + it("should include proper formatting instructions", () => { + const ruleInstructions: RuleInstruction[] = [ruleTypeDefinitions.general] + const options: RulesGenerationOptions = { + selectedRuleTypes: ["general"], + addToGitignore: false, + alwaysAllowWriteProtected: false, + includeCustomRules: false, + customRulesText: "", + } + + const result = generateRulesInstructions(ruleInstructions, options) + + expect(result).toContain("Make the rules actionable and specific") + expect(result).toContain("Build/lint/test commands") + expect(result).toContain("Code style guidelines") + expect(result).toContain("Error handling patterns") + expect(result).toContain("Keep rules concise") + expect(result).toContain("aim for 20 lines per file") + }) }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 6c300bcd56..fa14f477cc 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1888,11 +1888,7 @@ export const webviewMessageHandler = async ( try { const workspacePath = getWorkspacePath() if (!workspacePath) { - await provider.postMessageToWebview({ - type: "rulesGenerationStatus", - success: false, - error: "No workspace folder open", - }) + vscode.window.showErrorMessage("No workspace folder open. Please open a folder to generate rules.") break } @@ -1907,13 +1903,13 @@ export const webviewMessageHandler = async ( const includeCustomRules = message.includeCustomRules || false const customRulesText = message.customRulesText || "" - // Save current API config to restore later - const currentApiConfig = getGlobalState("currentApiConfigName") - - // Temporarily switch to the selected API config if provided - if (apiConfigName && apiConfigName !== currentApiConfig) { - await updateGlobalState("currentApiConfigName", apiConfigName) - await provider.postStateToWebview() + // Switch to the selected API config if provided + if (apiConfigName) { + const currentApiConfig = getGlobalState("currentApiConfigName") + if (apiConfigName !== currentApiConfig) { + await updateGlobalState("currentApiConfigName", apiConfigName) + await provider.activateProviderProfile({ name: apiConfigName }) + } } // Create a comprehensive message for the rules generation task using existing analysis logic @@ -1929,19 +1925,6 @@ export const webviewMessageHandler = async ( // Spawn a new task in code mode to generate the rules await provider.initClineWithTask(rulesGenerationMessage) - // Restore the original API config - if (apiConfigName && apiConfigName !== currentApiConfig) { - await updateGlobalState("currentApiConfigName", currentApiConfig) - await provider.postStateToWebview() - } - - // Send success message back to webview indicating task was created - await provider.postMessageToWebview({ - type: "rulesGenerationStatus", - success: true, - text: "Rules generation task created successfully. The new task will analyze your codebase and generate comprehensive rules.", - }) - // Automatically navigate to the chat tab to show the new task await provider.postMessageToWebview({ type: "action", @@ -1949,12 +1932,9 @@ export const webviewMessageHandler = async ( tab: "chat", }) } catch (error) { - // Send error message back to webview - await provider.postMessageToWebview({ - type: "rulesGenerationStatus", - success: false, - error: error instanceof Error ? error.message : String(error), - }) + // Show error message to user + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(`Failed to generate rules: ${errorMessage}`) } break case "checkExistingRuleFiles": diff --git a/src/services/rules/__tests__/rulesGenerator.test.ts b/src/services/rules/__tests__/rulesGenerator.test.ts index 29d15cda5b..86098fbe47 100644 --- a/src/services/rules/__tests__/rulesGenerator.test.ts +++ b/src/services/rules/__tests__/rulesGenerator.test.ts @@ -96,5 +96,86 @@ describe("rulesGenerator", () => { expect(message).toContain("Identify architectural patterns") expect(message).toContain("separation of concerns") }) + + it("should include custom rules when includeCustomRules is true", async () => { + const customRulesText = "Always use TypeScript interfaces instead of types" + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general"], + false, + false, + true, + customRulesText, + ) + + expect(message).toContain("Additional rules from User to add to the rules file:") + expect(message).toContain(customRulesText) + }) + + it("should not include custom rules when includeCustomRules is false", async () => { + const customRulesText = "Always use TypeScript interfaces instead of types" + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general"], + false, + false, + false, + customRulesText, + ) + + expect(message).not.toContain("Additional rules from User to add to the rules file:") + expect(message).not.toContain(customRulesText) + }) + + it("should handle empty custom rules text", async () => { + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general"], + false, + false, + true, + "", + ) + + expect(message).not.toContain("Additional rules from User to add to the rules file:") + }) + + it("should handle mkdir errors gracefully", async () => { + // Mock mkdir to throw an error + vi.mocked(fs.mkdir).mockRejectedValueOnce(new Error("Permission denied")) + + // Should not throw even if mkdir fails + await expect( + createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, true), + ).resolves.toBeDefined() + }) + + it("should filter out invalid rule types", async () => { + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general", "invalid-type", "code"], + false, + false, + ) + + // Should include valid types + expect(message).toContain(".roo/rules/coding-standards.md") + expect(message).toContain(".roo/rules-code/implementation-rules.md") + + // Should not include invalid type + expect(message).not.toContain("invalid-type") + }) + + it("should handle all rule types", async () => { + const allRuleTypes = ["general", "code", "architect", "debug", "docs-extractor"] + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, allRuleTypes, false, false) + + // Check all rule files are mentioned + expect(message).toContain(".roo/rules/coding-standards.md") + expect(message).toContain(".roo/rules-code/implementation-rules.md") + expect(message).toContain(".roo/rules-architect/architecture-rules.md") + expect(message).toContain(".roo/rules-debug/debugging-rules.md") + expect(message).toContain(".roo/rules-docs-extractor/documentation-rules.md") + }) }) }) diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index e8308157a1..d3224b79ee 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes, useState, useEffect } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { FileText, Loader2, AlertTriangle, Info, Sparkles } from "lucide-react" +import { FileText, AlertTriangle, Info, Sparkles } from "lucide-react" import { Button } from "@/components/ui/button" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" @@ -25,13 +25,7 @@ interface RuleType { export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesSettingsProps) => { const { t } = useAppTranslation() - const [isGenerating, setIsGenerating] = useState(false) - const [generationStatus, setGenerationStatus] = useState<{ - type: "success" | "error" | null - message: string - }>({ type: null, message: "" }) const [addToGitignore, setAddToGitignore] = useState(true) - const [_existingFiles, setExistingFiles] = useState([]) const [alwaysAllowWriteProtected, setAlwaysAllowWriteProtected] = useState(true) const [selectedApiConfig, setSelectedApiConfig] = useState("") const [includeCustomRules, setIncludeCustomRules] = useState(false) @@ -100,21 +94,7 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data - if (message.type === "rulesGenerationStatus") { - setIsGenerating(false) - if (message.success) { - setGenerationStatus({ - type: "success", - message: message.text || "", - }) - } else { - setGenerationStatus({ - type: "error", - message: message.error || "Unknown error occurred", - }) - } - } else if (message.type === "existingRuleFiles") { - setExistingFiles(message.files || []) + if (message.type === "existingRuleFiles") { // Update rule types with existence information setRuleTypes((prev) => prev.map((rule) => ({ @@ -141,16 +121,9 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS const handleGenerateRules = () => { const selectedRules = ruleTypes.filter((rule) => rule.checked) if (selectedRules.length === 0) { - setGenerationStatus({ - type: "error", - message: t("settings:rules.noRulesSelected"), - }) return } - setIsGenerating(true) - setGenerationStatus({ type: null, message: "" }) - // Send message to extension to generate rules vscode.postMessage({ type: "generateRules", @@ -165,6 +138,7 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS const existingRules = ruleTypes.filter((rule) => rule.checked && rule.exists) const hasExistingFiles = existingRules.length > 0 + const hasSelectedRules = ruleTypes.some((rule) => rule.checked) return (
@@ -347,51 +321,25 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS content={ hasUnsavedChanges ? t("settings:rules.unsavedChangesError") - : t("settings:rules.generateButtonTooltip") + : !hasSelectedRules + ? t("settings:rules.noRulesSelected") + : t("settings:rules.generateButtonTooltip") }>
- - {isGenerating && ( -

- {t("settings:rules.creatingTaskDescription")} -

- )} - - {generationStatus.type === "success" && ( -
-

{generationStatus.message || t("settings:rules.taskCreated")}

-
- )} - - {generationStatus.type === "error" && ( -
-

{t("settings:rules.error")}

-

- {t("settings:rules.errorDescription", { error: generationStatus.message })} -

-
- )} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 55a826c987..f5b81696bc 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -611,19 +611,6 @@ }, "generateButton": "Generate Rules", "generateButtonTooltip": "Create a new task to analyze the codebase and generate rules automatically", - "generating": "Creating task...", - "generatingDescription": "Analyzing your codebase to create comprehensive rules. This may take a moment.", - "creatingTaskDescription": "Creating a new task to analyze your codebase and generate comprehensive rules.", - "success": "Rules generated successfully!", - "successDescription": "Rules have been saved to {{path}}", - "taskCreated": "Rules generation task created! You'll be automatically taken to the new task.", - "error": "Failed to create rules generation task", - "errorDescription": "An error occurred while creating the rules generation task: {{error}}", - "viewRules": "View Generated Rules", - "existingRules": "Existing rules detected", - "existingRulesDescription": "Rules already exist at {{path}}. Generating new rules will create a timestamped file to preserve your existing rules.", - "noWorkspace": "No workspace folder open", - "noWorkspaceDescription": "Please open a workspace folder to generate rules for your project.", "selectTypes": "Select rule types to generate:", "noRulesSelected": "Please select at least one rule type to generate", "unsavedChangesError": "Please save your settings before generating rules",