From 83903797ccc403e1185d447b9fc6aa4297e72c7d Mon Sep 17 00:00:00 2001 From: Will Li Date: Thu, 17 Jul 2025 13:10:30 -0700 Subject: [PATCH] basically working --- src/core/webview/webviewMessageHandler.ts | 67 ++++- src/services/rules/rulesGenerator.ts | 95 ++++++- src/shared/ExtensionMessage.ts | 2 + src/shared/WebviewMessage.ts | 6 + .../src/components/settings/RulesSettings.tsx | 260 ++++++++++++++++-- webview-ui/src/i18n/locales/en/settings.json | 35 ++- 6 files changed, 433 insertions(+), 32 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index f389ce602a..11e208bbed 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1899,12 +1899,38 @@ export const webviewMessageHandler = async ( // Import the rules generation service const { createRulesGenerationTaskMessage } = await import("../../services/rules/rulesGenerator") + // Get selected rule types and options from the message + const selectedRuleTypes = message.selectedRuleTypes || ["general"] + const addToGitignore = message.addToGitignore || false + const alwaysAllowWriteProtected = message.alwaysAllowWriteProtected || false + const apiConfigName = message.apiConfigName + + // 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() + } + // Create a comprehensive message for the rules generation task using existing analysis logic - const rulesGenerationMessage = await createRulesGenerationTaskMessage(workspacePath) + const rulesGenerationMessage = await createRulesGenerationTaskMessage( + workspacePath, + selectedRuleTypes, + addToGitignore, + alwaysAllowWriteProtected, + ) // 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", @@ -1927,6 +1953,45 @@ export const webviewMessageHandler = async ( }) } break + case "checkExistingRuleFiles": + // Check which rule files already exist + try { + const workspacePath = getWorkspacePath() + if (!workspacePath) { + break + } + + const { fileExistsAtPath } = await import("../../utils/fs") + const path = await import("path") + + const ruleTypeToPath: Record = { + general: path.join(workspacePath, ".roo", "rules", "coding-standards.md"), + code: path.join(workspacePath, ".roo", "rules-code", "implementation-rules.md"), + architect: path.join(workspacePath, ".roo", "rules-architect", "architecture-rules.md"), + debug: path.join(workspacePath, ".roo", "rules-debug", "debugging-rules.md"), + "docs-extractor": path.join( + workspacePath, + ".roo", + "rules-docs-extractor", + "documentation-rules.md", + ), + } + + const existingFiles: string[] = [] + for (const [type, filePath] of Object.entries(ruleTypeToPath)) { + if (await fileExistsAtPath(filePath)) { + existingFiles.push(type) + } + } + + await provider.postMessageToWebview({ + type: "existingRuleFiles", + files: existingFiles, + }) + } catch (error) { + // Silently fail - not critical + } + break case "humanRelayResponse": if (message.requestId && message.text) { vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), { diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index 1104c4c34b..4ea649a02d 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -204,19 +204,73 @@ async function generateCodebaseSummary(workspacePath: string, config: ProjectCon /** * Creates a comprehensive task message for rules generation that can be used with initClineWithTask */ -export async function createRulesGenerationTaskMessage(workspacePath: string): Promise { +export async function createRulesGenerationTaskMessage( + workspacePath: string, + selectedRuleTypes: string[], + addToGitignore: boolean, + alwaysAllowWriteProtected: boolean = false, +): Promise { // Analyze the project to get context const config = await analyzeProjectConfig(workspacePath) const codebaseSummary = await generateCodebaseSummary(workspacePath, config) - // Ensure .roo/rules directory exists at project root - const rooRulesDir = path.join(workspacePath, ".roo", "rules") - try { - await fs.mkdir(rooRulesDir, { recursive: true }) - } catch (error) { - // Directory might already exist, which is fine + // Ensure all necessary directories exist at project root + const directoriesToCreate = [ + path.join(workspacePath, ".roo", "rules"), + path.join(workspacePath, ".roo", "rules-code"), + path.join(workspacePath, ".roo", "rules-architect"), + path.join(workspacePath, ".roo", "rules-debug"), + path.join(workspacePath, ".roo", "rules-docs-extractor"), + ] + + for (const dir of directoriesToCreate) { + try { + await fs.mkdir(dir, { recursive: true }) + } catch (error) { + // Directory might already exist, which is fine + } } + // Create rule-specific instructions based on selected types + interface RuleInstruction { + path: string + focus: string + } + + const ruleInstructions: RuleInstruction[] = selectedRuleTypes + .map((type) => { + switch (type) { + case "general": + return { + path: ".roo/rules/coding-standards.md", + focus: "General coding standards that apply to all modes, including naming conventions, file organization, and general best practices", + } + case "code": + return { + path: ".roo/rules-code/implementation-rules.md", + focus: "Specific rules for code implementation, focusing on syntax patterns, code structure, error handling, testing approaches, and detailed implementation guidelines", + } + case "architect": + return { + path: ".roo/rules-architect/architecture-rules.md", + focus: "High-level system design rules, focusing on file layout, module organization, architectural patterns, and system-wide design principles", + } + case "debug": + return { + path: ".roo/rules-debug/debugging-rules.md", + focus: "Debugging workflow rules, including error investigation approaches, logging strategies, troubleshooting patterns, and debugging best practices", + } + case "docs-extractor": + return { + path: ".roo/rules-docs-extractor/documentation-rules.md", + focus: "Documentation extraction and formatting rules, including documentation style guides, API documentation patterns, and content organization", + } + default: + return null + } + }) + .filter((rule): rule is RuleInstruction => rule !== null) + // Create a comprehensive message for the rules generation task const taskMessage = `Analyze this codebase and generate comprehensive rules for AI agents working in this repository. @@ -233,10 +287,17 @@ ${codebaseSummary} - Project-specific conventions and best practices - File organization patterns -3. **Save the rules** to the file at exactly this path: .roo/rules/coding-standards.md - - The .roo/rules directory has already been created for you - - Always overwrite the existing file if it exists - - Use the \`write_to_file\` tool to save the content +3. **Generate and save the following rule files**: +${ruleInstructions + .map( + (rule, index) => ` + ${index + 1}. **${rule.path}** + - Focus: ${rule.focus} + - The directory has already been created for you + - Always overwrite the existing file if it exists + - Use the \`write_to_file\` tool to save the content${alwaysAllowWriteProtected ? "\n - Note: Auto-approval for protected file writes is enabled, so you can write to .roo directories without manual approval" : ""}`, + ) + .join("\n")} 4. **Open the generated file** in the editor for review @@ -244,7 +305,17 @@ The rules should be about 20-30 lines long and focus on the most important guide If there are existing rules files (like CLAUDE.md, .cursorrules, .cursor/rules, .github/copilot-instructions.md), incorporate and improve upon them. -Use the \`safeWriteJson\` utility from \`src/utils/safeWriteJson.ts\` for any JSON file operations to ensure atomic writes.` +Use the \`safeWriteJson\` utility from \`src/utils/safeWriteJson.ts\` for any JSON file operations to ensure atomic writes. + +${ + addToGitignore + ? `5. **Add the generated files to .gitignore**: + - After generating all rule files, add entries to .gitignore to prevent them from being committed + - Add each generated file path to .gitignore (e.g., .roo/rules/coding-standards.md) + - If .gitignore doesn't exist, create it + - If the entries already exist in .gitignore, don't duplicate them` + : "" +}` return taskMessage } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index fe43364208..35add00e37 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -106,8 +106,10 @@ export interface ExtensionMessage { | "codeIndexSettingsSaved" | "codeIndexSecretStatus" | "rulesGenerationStatus" + | "existingRuleFiles" text?: string payload?: any // Add a generic payload for now, can refine later + files?: string[] // For existingRuleFiles action?: | "chatButtonClicked" | "mcpButtonClicked" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 466376d394..6d33dd142f 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -195,6 +195,7 @@ export interface WebviewMessage { | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" | "generateRules" + | "checkExistingRuleFiles" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -236,6 +237,11 @@ export interface WebviewMessage { visibility?: ShareVisibility // For share visibility hasContent?: boolean // For checkRulesDirectoryResult checkOnly?: boolean // For deleteCustomMode check + selectedRuleTypes?: string[] // For generateRules + addToGitignore?: boolean // For generateRules + alwaysAllowWriteProtected?: boolean // For generateRules + apiConfigName?: string // For generateRules + files?: string[] // For existingRuleFiles response codeIndexSettings?: { // Global state settings codebaseIndexEnabled: boolean diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index 01909ac80f..a2d3c3dcde 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -1,15 +1,25 @@ import { HTMLAttributes, useState, useEffect } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { FileText, Loader2 } from "lucide-react" +import { FileText, Loader2, AlertTriangle, Info } from "lucide-react" import { Button } from "@/components/ui/button" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" type RulesSettingsProps = HTMLAttributes +interface RuleType { + id: string + label: string + description: string + checked: boolean + exists?: boolean +} + export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { const { t } = useAppTranslation() const [isGenerating, setIsGenerating] = useState(false) @@ -17,6 +27,69 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { type: "success" | "error" | null message: string }>({ type: null, message: "" }) + const [addToGitignore, setAddToGitignore] = useState(false) + const [_existingFiles, setExistingFiles] = useState([]) + const [alwaysAllowWriteProtected, setAlwaysAllowWriteProtected] = useState(false) + const [selectedApiConfig, setSelectedApiConfig] = useState("") + + const { listApiConfigMeta, currentApiConfigName } = useExtensionState() + + const [ruleTypes, setRuleTypes] = useState([ + { + id: "general", + label: t("settings:rules.types.general.label"), + description: t("settings:rules.types.general.description"), + checked: true, + exists: false, + }, + { + id: "code", + label: t("settings:rules.types.code.label"), + description: t("settings:rules.types.code.description"), + checked: true, + exists: false, + }, + { + id: "architect", + label: t("settings:rules.types.architect.label"), + description: t("settings:rules.types.architect.description"), + checked: true, + exists: false, + }, + { + id: "debug", + label: t("settings:rules.types.debug.label"), + description: t("settings:rules.types.debug.description"), + checked: true, + exists: false, + }, + { + id: "docs-extractor", + label: t("settings:rules.types.docsExtractor.label"), + description: t("settings:rules.types.docsExtractor.description"), + checked: true, + exists: false, + }, + ]) + + const handleRuleTypeToggle = (id: string) => { + setRuleTypes((prev) => prev.map((rule) => (rule.id === id ? { ...rule, checked: !rule.checked } : rule))) + } + + // Check for existing files and get current settings when component mounts + useEffect(() => { + vscode.postMessage({ + type: "checkExistingRuleFiles", + }) + + // Request current state to get alwaysAllowWriteProtected value + vscode.postMessage({ type: "webviewDidLaunch" }) + + // Set default API config + if (currentApiConfigName && !selectedApiConfig) { + setSelectedApiConfig(currentApiConfigName) + } + }, [currentApiConfigName, selectedApiConfig]) useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -34,6 +107,20 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { message: message.error || "Unknown error occurred", }) } + } else if (message.type === "existingRuleFiles") { + setExistingFiles(message.files || []) + // Update rule types with existence information + setRuleTypes((prev) => + prev.map((rule) => ({ + ...rule, + exists: message.files?.includes(rule.id) || false, + })), + ) + } else if (message.type === "state") { + // Update alwaysAllowWriteProtected from the extension state + if (message.state?.alwaysAllowWriteProtected !== undefined) { + setAlwaysAllowWriteProtected(message.state.alwaysAllowWriteProtected) + } } } @@ -42,15 +129,31 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { }, []) 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", + selectedRuleTypes: selectedRules.map((rule) => rule.id), + addToGitignore, + alwaysAllowWriteProtected, + apiConfigName: selectedApiConfig, }) } + const existingRules = ruleTypes.filter((rule) => rule.checked && rule.exists) + const hasExistingFiles = existingRules.length > 0 + return (
@@ -64,23 +167,144 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => {

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

-
- + {/* Recommendation box */} +
+ +
+ {t("settings:rules.autoApproveRecommendation")} +
+
+ +
+
+

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

+
+ {ruleTypes.map((ruleType) => ( +
handleRuleTypeToggle(ruleType.id)} + className={cn( + "relative p-3 rounded-md border cursor-pointer transition-all", + "hover:border-vscode-focusBorder", + ruleType.checked + ? "bg-vscode-list-activeSelectionBackground border-vscode-focusBorder" + : "bg-vscode-editor-background border-vscode-panel-border", + )}> +
+
+ {ruleType.label} + {ruleType.exists && ( + + • + + )} +
+
+ {ruleType.description} +
+
+
+ ))} +
+
+ + {hasExistingFiles && ( +
+ +
+
{t("settings:rules.overwriteWarning")}
+
    + {existingRules.map((rule) => ( +
  • {rule.label}
  • + ))} +
+
+
+ )} + +
+ +
+ +
+ +
+ +
+
+
+ + +
+ + +
+
{isGenerating && (

diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 284429a327..fc58c4f659 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -619,7 +619,40 @@ "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." + "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", + "types": { + "general": { + "label": "General Rules", + "description": "General coding standards applied to all modes" + }, + "code": { + "label": "Code Mode Rules", + "description": "Specific rules for code syntax, implementation details, and best practices" + }, + "architect": { + "label": "Architect Mode Rules", + "description": "Rules for high-level system design, file organization, and architecture patterns" + }, + "debug": { + "label": "Debug Mode Rules", + "description": "Rules for debugging workflows, error handling, and troubleshooting approaches" + }, + "docsExtractor": { + "label": "Documentation Rules", + "description": "Rules for documentation extraction and formatting standards" + } + }, + "fileExists": "File exists", + "overwriteWarning": "Warning: The following rule files already exist and will be overwritten:", + "addToGitignore": "Add to .gitignore", + "addToGitignoreDescription": "Automatically add the generated rule files to .gitignore to prevent them from being committed to version control", + "autoApproveProtected": "Auto-approve protected file writes", + "autoApproveProtectedDescription": "Allow Roo to write to .roo directory without requiring manual approval", + "autoApproveRecommendation": "For the best experience generating rules, we recommend enabling auto-approve for both read and write operations in the Auto-Approve settings above.", + "apiConfigLabel": "API Configuration", + "selectApiConfig": "Select API configuration" }, "promptCaching": { "label": "Disable prompt caching",