From e09afaa4836569cc3e0a862172534ac1c5a12628 Mon Sep 17 00:00:00 2001 From: Will Li Date: Wed, 16 Jul 2025 08:40:14 -0700 Subject: [PATCH 01/13] initial working --- src/core/webview/webviewMessageHandler.ts | 34 +++ src/services/rules/rulesGenerator.ts | 289 ++++++++++++++++++ src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../settings/ExperimentalSettings.tsx | 3 + .../src/components/settings/RulesSettings.tsx | 113 +++++++ webview-ui/src/i18n/locales/en/settings.json | 17 ++ 7 files changed, 458 insertions(+) create mode 100644 src/services/rules/rulesGenerator.ts create mode 100644 webview-ui/src/components/settings/RulesSettings.tsx diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e70b39df8f..70a9a4753e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1883,6 +1883,40 @@ export const webviewMessageHandler = async ( }) } break + case "generateRules": + // Generate rules for the current workspace + try { + const workspacePath = getWorkspacePath() + if (!workspacePath) { + await provider.postMessageToWebview({ + type: "rulesGenerationStatus", + success: false, + error: "No workspace folder open", + }) + break + } + + // Import the rules generation service + const { generateRulesForWorkspace } = await import("../../services/rules/rulesGenerator") + + // Generate the rules + const rulesPath = await generateRulesForWorkspace(workspacePath) + + // Send success message back to webview + await provider.postMessageToWebview({ + type: "rulesGenerationStatus", + success: true, + text: rulesPath, + }) + } catch (error) { + // Send error message back to webview + await provider.postMessageToWebview({ + type: "rulesGenerationStatus", + success: false, + error: error instanceof Error ? error.message : String(error), + }) + } + 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 new file mode 100644 index 0000000000..a85df2d0c6 --- /dev/null +++ b/src/services/rules/rulesGenerator.ts @@ -0,0 +1,289 @@ +import * as fs from "fs/promises" +import * as path from "path" +import * as vscode from "vscode" +import { fileExistsAtPath } from "../../utils/fs" +import { getProjectRooDirectoryForCwd } from "../roo-config/index" + +interface ProjectConfig { + type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" + hasTypeScript: boolean + hasESLint: boolean + hasPrettier: boolean + hasJest: boolean + hasVitest: boolean + hasPytest: boolean + packageManager: "npm" | "yarn" | "pnpm" | "bun" | null + dependencies: string[] + devDependencies: string[] + scripts: Record +} + +/** + * Analyzes the project configuration files to determine project type and tools + */ +async function analyzeProjectConfig(workspacePath: string): Promise { + const config: ProjectConfig = { + type: "unknown", + hasTypeScript: false, + hasESLint: false, + hasPrettier: false, + hasJest: false, + hasVitest: false, + hasPytest: false, + packageManager: null, + dependencies: [], + devDependencies: [], + scripts: {}, + } + + // Check for package.json + const packageJsonPath = path.join(workspacePath, "package.json") + if (await fileExistsAtPath(packageJsonPath)) { + try { + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")) + + // Determine package manager + if (await fileExistsAtPath(path.join(workspacePath, "yarn.lock"))) { + config.packageManager = "yarn" + } else if (await fileExistsAtPath(path.join(workspacePath, "pnpm-lock.yaml"))) { + config.packageManager = "pnpm" + } else if (await fileExistsAtPath(path.join(workspacePath, "bun.lockb"))) { + config.packageManager = "bun" + } else if (await fileExistsAtPath(path.join(workspacePath, "package-lock.json"))) { + config.packageManager = "npm" + } + + // Extract dependencies + config.dependencies = Object.keys(packageJson.dependencies || {}) + config.devDependencies = Object.keys(packageJson.devDependencies || {}) + config.scripts = packageJson.scripts || {} + + // Check for specific tools + const allDeps = [...config.dependencies, ...config.devDependencies] + config.hasTypeScript = + allDeps.includes("typescript") || (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) + config.hasESLint = + allDeps.includes("eslint") || + (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.js"))) || + (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.json"))) + config.hasPrettier = + allDeps.includes("prettier") || (await fileExistsAtPath(path.join(workspacePath, ".prettierrc"))) + config.hasJest = allDeps.includes("jest") + config.hasVitest = allDeps.includes("vitest") + + // Determine project type + if (config.hasTypeScript) { + config.type = "typescript" + } else { + config.type = "javascript" + } + } catch (error) { + console.error("Error parsing package.json:", error) + } + } + + // Check for Python project + if ( + (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) || + (await fileExistsAtPath(path.join(workspacePath, "setup.py"))) || + (await fileExistsAtPath(path.join(workspacePath, "requirements.txt"))) + ) { + config.type = "python" + config.hasPytest = + (await fileExistsAtPath(path.join(workspacePath, "pytest.ini"))) || + (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) + } + + // Check for other project types + if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { + config.type = "go" + } else if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { + config.type = "rust" + } else if ( + (await fileExistsAtPath(path.join(workspacePath, "pom.xml"))) || + (await fileExistsAtPath(path.join(workspacePath, "build.gradle"))) + ) { + config.type = "java" + } + + return config +} + +/** + * Generates rules content based on project analysis + */ +function generateRulesContent(config: ProjectConfig, workspacePath: string): string { + const sections: string[] = [] + + // Header + sections.push("# Project Rules") + sections.push("") + sections.push(`Generated on: ${new Date().toISOString()}`) + sections.push(`Project type: ${config.type}`) + sections.push("") + + // Build and Development + sections.push("## Build and Development") + sections.push("") + + if (config.packageManager) { + sections.push(`- Package manager: ${config.packageManager}`) + sections.push(`- Install dependencies: \`${config.packageManager} install\``) + + if (config.scripts.build) { + sections.push(`- Build command: \`${config.packageManager} run build\``) + } + if (config.scripts.test) { + sections.push(`- Test command: \`${config.packageManager} run test\``) + } + if (config.scripts.dev || config.scripts.start) { + const devScript = config.scripts.dev || config.scripts.start + sections.push( + `- Development server: \`${config.packageManager} run ${config.scripts.dev ? "dev" : "start"}\``, + ) + } + } + + sections.push("") + + // Code Style and Linting + sections.push("## Code Style and Linting") + sections.push("") + + if (config.hasESLint) { + sections.push("- ESLint is configured for this project") + sections.push("- Run linting: `npm run lint` (if configured)") + sections.push("- Follow ESLint rules and fix any linting errors before committing") + } + + if (config.hasPrettier) { + sections.push("- Prettier is configured for code formatting") + sections.push("- Format code before committing") + sections.push("- Run formatting: `npm run format` (if configured)") + } + + if (config.hasTypeScript) { + sections.push("- TypeScript is used in this project") + sections.push("- Ensure all TypeScript errors are resolved before committing") + sections.push("- Use proper type annotations and avoid `any` types") + sections.push("- Run type checking: `npm run type-check` or `tsc --noEmit`") + } + + sections.push("") + + // Testing + sections.push("## Testing") + sections.push("") + + if (config.hasJest || config.hasVitest) { + const testFramework = config.hasVitest ? "Vitest" : "Jest" + sections.push(`- ${testFramework} is used for testing`) + sections.push("- Write tests for new features and bug fixes") + sections.push("- Ensure all tests pass before committing") + sections.push(`- Run tests: \`${config.packageManager || "npm"} run test\``) + + if (config.hasVitest) { + sections.push("- Vitest specific: Test files should use `.test.ts` or `.spec.ts` extensions") + sections.push("- The `describe`, `test`, `it` functions are globally available") + } + } + + if (config.hasPytest && config.type === "python") { + sections.push("- Pytest is used for testing") + sections.push("- Write tests in `test_*.py` or `*_test.py` files") + sections.push("- Run tests: `pytest`") + } + + sections.push("") + + // Project Structure + sections.push("## Project Structure") + sections.push("") + sections.push("- Follow the existing project structure and naming conventions") + sections.push("- Place new files in appropriate directories") + sections.push("- Use consistent file naming (kebab-case, camelCase, or PascalCase as per project convention)") + + sections.push("") + + // Language-specific rules + if (config.type === "typescript" || config.type === "javascript") { + sections.push("## JavaScript/TypeScript Guidelines") + sections.push("") + sections.push("- Use ES6+ syntax (const/let, arrow functions, destructuring, etc.)") + sections.push("- Prefer functional programming patterns where appropriate") + sections.push("- Handle errors properly with try/catch blocks") + sections.push("- Use async/await for asynchronous operations") + sections.push("- Follow existing import/export patterns") + sections.push("") + } + + if (config.type === "python") { + sections.push("## Python Guidelines") + sections.push("") + sections.push("- Follow PEP 8 style guide") + sections.push("- Use type hints where appropriate") + sections.push("- Write docstrings for functions and classes") + sections.push("- Use virtual environments for dependency management") + sections.push("") + } + + // General Best Practices + sections.push("## General Best Practices") + sections.push("") + sections.push("- Write clear, self-documenting code") + sections.push("- Add comments for complex logic") + sections.push("- Keep functions small and focused") + sections.push("- Follow DRY (Don't Repeat Yourself) principle") + sections.push("- Handle edge cases and errors gracefully") + sections.push("- Write meaningful commit messages") + sections.push("") + + // Dependencies + if (config.dependencies.length > 0 || config.devDependencies.length > 0) { + sections.push("## Key Dependencies") + sections.push("") + + // List some key dependencies + const keyDeps = [...config.dependencies, ...config.devDependencies] + .filter((dep) => !dep.startsWith("@types/")) + .slice(0, 10) + + keyDeps.forEach((dep) => { + sections.push(`- ${dep}`) + }) + + sections.push("") + } + + return sections.join("\n") +} + +/** + * Generates rules for the workspace and saves them to a file + */ +export async function generateRulesForWorkspace(workspacePath: string): Promise { + // Analyze the project + const config = await analyzeProjectConfig(workspacePath) + + // Generate rules content + const rulesContent = generateRulesContent(config, workspacePath) + + // Ensure .roo/rules directory exists + const rooDir = getProjectRooDirectoryForCwd(workspacePath) + const rulesDir = path.join(rooDir, "rules") + await fs.mkdir(rulesDir, { recursive: true }) + + // Generate filename with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5) + const rulesFileName = `generated-rules-${timestamp}.md` + const rulesPath = path.join(rulesDir, rulesFileName) + + // Write rules file + await fs.writeFile(rulesPath, rulesContent, "utf-8") + + // Open the file in VSCode + const doc = await vscode.workspace.openTextDocument(rulesPath) + await vscode.window.showTextDocument(doc) + + return rulesPath +} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 833c51336b..fe43364208 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -105,6 +105,7 @@ export interface ExtensionMessage { | "shareTaskSuccess" | "codeIndexSettingsSaved" | "codeIndexSecretStatus" + | "rulesGenerationStatus" text?: string payload?: any // Add a generic payload for now, can refine later action?: diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d5dc3f8c28..466376d394 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -194,6 +194,7 @@ export interface WebviewMessage { | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" + | "generateRules" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 53801232ec..ea59ab89f8 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -12,6 +12,7 @@ import { SetExperimentEnabled } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { ExperimentalFeature } from "./ExperimentalFeature" +import { RulesSettings } from "./RulesSettings" type ExperimentalSettingsProps = HTMLAttributes & { experiments: Experiments @@ -66,6 +67,8 @@ export const ExperimentalSettings = ({ ) })} + + ) } diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx new file mode 100644 index 0000000000..9222476f25 --- /dev/null +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -0,0 +1,113 @@ +import { HTMLAttributes, useState, useEffect } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { FileText, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { vscode } from "@/utils/vscode" +import { cn } from "@/lib/utils" + +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +type RulesSettingsProps = HTMLAttributes + +export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { + const { t } = useAppTranslation() + const [isGenerating, setIsGenerating] = useState(false) + const [generationStatus, setGenerationStatus] = useState<{ + type: "success" | "error" | null + message: string + }>({ type: null, message: "" }) + + 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", + }) + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const handleGenerateRules = () => { + setIsGenerating(true) + setGenerationStatus({ type: null, message: "" }) + + // Send message to extension to generate rules + vscode.postMessage({ + type: "generateRules", + }) + } + + return ( +
+ +
+ +
{t("settings:rules.title")}
+
+
+ +
+
+

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

+ +
+ + + {isGenerating && ( +

+ {t("settings:rules.generatingDescription")} +

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

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

+

+ {t("settings:rules.successDescription", { path: generationStatus.message })} +

+
+ )} + + {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 fa1fbab13c..9c0a9c6935 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -602,6 +602,23 @@ "description": "When enabled, Roo can edit multiple files in a single request. When disabled, Roo must edit files one at a time. Disabling this can help when working with less capable models or when you want more control over file modifications." } }, + "rules": { + "title": "Rules", + "description": "Configure automatic rules generation for your codebase. Rules help AI agents understand your project's conventions and best practices.", + "generateButton": "Generate Rules", + "generateButtonTooltip": "Analyze the codebase and generate rules automatically", + "generating": "Generating rules...", + "generatingDescription": "Analyzing your codebase to create comprehensive rules. This may take a moment.", + "success": "Rules generated successfully!", + "successDescription": "Rules have been saved to {{path}}", + "error": "Failed to generate rules", + "errorDescription": "An error occurred while generating rules: {{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." + }, "promptCaching": { "label": "Disable prompt caching", "description": "When checked, Roo will not use prompt caching for this model." From beb5f7a73c23ce7575a907a6ad540a7e670d469b Mon Sep 17 00:00:00 2001 From: Will Li Date: Thu, 17 Jul 2025 11:23:53 -0700 Subject: [PATCH 02/13] LLM working --- src/core/webview/webviewMessageHandler.ts | 7 +- src/services/rules/rulesGenerator.ts | 220 ++++++++++++++++------ 2 files changed, 172 insertions(+), 55 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 70a9a4753e..e0602f16a5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1896,11 +1896,14 @@ export const webviewMessageHandler = async ( break } + // Get the current API configuration + const { apiConfiguration } = await provider.getState() + // Import the rules generation service const { generateRulesForWorkspace } = await import("../../services/rules/rulesGenerator") - // Generate the rules - const rulesPath = await generateRulesForWorkspace(workspacePath) + // Generate the rules with LLM support + const rulesPath = await generateRulesForWorkspace(workspacePath, apiConfiguration) // Send success message back to webview await provider.postMessageToWebview({ diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index a85df2d0c6..fe57cfc7a4 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -1,8 +1,10 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" +import type { ProviderSettings } from "@roo-code/types" import { fileExistsAtPath } from "../../utils/fs" import { getProjectRooDirectoryForCwd } from "../roo-config/index" +import { singleCompletionHandler } from "../../utils/single-completion-handler" interface ProjectConfig { type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" @@ -110,9 +112,166 @@ async function analyzeProjectConfig(workspacePath: string): Promise { + const summary: string[] = [] + + // Project structure overview + summary.push("# Codebase Analysis") + summary.push("") + summary.push(`**Project Type**: ${config.type}`) + summary.push(`**Package Manager**: ${config.packageManager || "none"}`) + summary.push("") + + // Configuration files + summary.push("## Configuration Files Found:") + const configFiles: string[] = [] + + if (await fileExistsAtPath(path.join(workspacePath, "package.json"))) { + configFiles.push("package.json") + } + if (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) { + configFiles.push("tsconfig.json") + } + if (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.js"))) { + configFiles.push(".eslintrc.js") + } + if (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.json"))) { + configFiles.push(".eslintrc.json") + } + if (await fileExistsAtPath(path.join(workspacePath, ".prettierrc"))) { + configFiles.push(".prettierrc") + } + if (await fileExistsAtPath(path.join(workspacePath, "jest.config.js"))) { + configFiles.push("jest.config.js") + } + if (await fileExistsAtPath(path.join(workspacePath, "vitest.config.ts"))) { + configFiles.push("vitest.config.ts") + } + if (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) { + configFiles.push("pyproject.toml") + } + if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { + configFiles.push("Cargo.toml") + } + if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { + configFiles.push("go.mod") + } + + configFiles.forEach((file) => summary.push(`- ${file}`)) + summary.push("") + + // Dependencies + if (config.dependencies.length > 0) { + summary.push("## Key Dependencies:") + config.dependencies.slice(0, 10).forEach((dep) => summary.push(`- ${dep}`)) + summary.push("") + } + + if (config.devDependencies.length > 0) { + summary.push("## Dev Dependencies:") + config.devDependencies.slice(0, 10).forEach((dep) => summary.push(`- ${dep}`)) + summary.push("") + } + + // Scripts + if (Object.keys(config.scripts).length > 0) { + summary.push("## Available Scripts:") + Object.entries(config.scripts).forEach(([name, command]) => { + summary.push(`- **${name}**: \`${command}\``) + }) + summary.push("") + } + + // Tools detected + summary.push("## Tools Detected:") + const tools: string[] = [] + if (config.hasTypeScript) tools.push("TypeScript") + if (config.hasESLint) tools.push("ESLint") + if (config.hasPrettier) tools.push("Prettier") + if (config.hasJest) tools.push("Jest") + if (config.hasVitest) tools.push("Vitest") + if (config.hasPytest) tools.push("Pytest") + + tools.forEach((tool) => summary.push(`- ${tool}`)) + summary.push("") + + // Check for existing rules files + const existingRulesFiles: string[] = [] + if (await fileExistsAtPath(path.join(workspacePath, "CLAUDE.md"))) { + existingRulesFiles.push("CLAUDE.md") + } + if (await fileExistsAtPath(path.join(workspacePath, ".cursorrules"))) { + existingRulesFiles.push(".cursorrules") + } + if (await fileExistsAtPath(path.join(workspacePath, ".cursor", "rules"))) { + existingRulesFiles.push(".cursor/rules") + } + if (await fileExistsAtPath(path.join(workspacePath, ".github", "copilot-instructions.md"))) { + existingRulesFiles.push(".github/copilot-instructions.md") + } + + if (existingRulesFiles.length > 0) { + summary.push("## Existing Rules Files:") + existingRulesFiles.forEach((file) => summary.push(`- ${file}`)) + summary.push("") + } + + return summary.join("\n") +} + +/** + * Generates rules content using LLM analysis with fallback to deterministic approach + */ +async function generateRulesWithLLM( + workspacePath: string, + config: ProjectConfig, + apiConfiguration?: ProviderSettings, +): Promise { + if (!apiConfiguration) { + // Fallback to deterministic generation + return generateDeterministicRules(config, workspacePath) + } + + try { + const codebaseSummary = await generateCodebaseSummary(workspacePath, config) + + const prompt = `Please analyze this codebase and create a comprehensive rules file containing: + +1. Build/lint/test commands - especially for running a single test +2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc. +3. Project-specific conventions and best practices + +The file you create will be given to agentic coding agents that operate in this repository. Make it about 20 lines long and focus on the most important rules for this specific project. + +If there are existing rules files mentioned below, make sure to incorporate and improve upon them. + +Here's the codebase analysis: + +${codebaseSummary} + +Please respond with only the rules content in markdown format, starting with "# Project Rules".` + + const llmResponse = await singleCompletionHandler(apiConfiguration, prompt) + + // Validate that we got a reasonable response + if (llmResponse && llmResponse.trim().length > 100 && llmResponse.includes("# Project Rules")) { + return llmResponse.trim() + } else { + console.warn("LLM response was invalid, falling back to deterministic generation") + return generateDeterministicRules(config, workspacePath) + } + } catch (error) { + console.error("Error generating rules with LLM, falling back to deterministic generation:", error) + return generateDeterministicRules(config, workspacePath) + } +} + +/** + * Generates rules content deterministically (fallback approach) + */ +function generateDeterministicRules(config: ProjectConfig, workspacePath: string): string { const sections: string[] = [] // Header @@ -196,37 +355,6 @@ function generateRulesContent(config: ProjectConfig, workspacePath: string): str sections.push("") - // Project Structure - sections.push("## Project Structure") - sections.push("") - sections.push("- Follow the existing project structure and naming conventions") - sections.push("- Place new files in appropriate directories") - sections.push("- Use consistent file naming (kebab-case, camelCase, or PascalCase as per project convention)") - - sections.push("") - - // Language-specific rules - if (config.type === "typescript" || config.type === "javascript") { - sections.push("## JavaScript/TypeScript Guidelines") - sections.push("") - sections.push("- Use ES6+ syntax (const/let, arrow functions, destructuring, etc.)") - sections.push("- Prefer functional programming patterns where appropriate") - sections.push("- Handle errors properly with try/catch blocks") - sections.push("- Use async/await for asynchronous operations") - sections.push("- Follow existing import/export patterns") - sections.push("") - } - - if (config.type === "python") { - sections.push("## Python Guidelines") - sections.push("") - sections.push("- Follow PEP 8 style guide") - sections.push("- Use type hints where appropriate") - sections.push("- Write docstrings for functions and classes") - sections.push("- Use virtual environments for dependency management") - sections.push("") - } - // General Best Practices sections.push("## General Best Practices") sections.push("") @@ -238,35 +366,21 @@ function generateRulesContent(config: ProjectConfig, workspacePath: string): str sections.push("- Write meaningful commit messages") sections.push("") - // Dependencies - if (config.dependencies.length > 0 || config.devDependencies.length > 0) { - sections.push("## Key Dependencies") - sections.push("") - - // List some key dependencies - const keyDeps = [...config.dependencies, ...config.devDependencies] - .filter((dep) => !dep.startsWith("@types/")) - .slice(0, 10) - - keyDeps.forEach((dep) => { - sections.push(`- ${dep}`) - }) - - sections.push("") - } - return sections.join("\n") } /** * Generates rules for the workspace and saves them to a file */ -export async function generateRulesForWorkspace(workspacePath: string): Promise { +export async function generateRulesForWorkspace( + workspacePath: string, + apiConfiguration?: ProviderSettings, +): Promise { // Analyze the project const config = await analyzeProjectConfig(workspacePath) - // Generate rules content - const rulesContent = generateRulesContent(config, workspacePath) + // Generate rules content using LLM with fallback + const rulesContent = await generateRulesWithLLM(workspacePath, config, apiConfiguration) // Ensure .roo/rules directory exists const rooDir = getProjectRooDirectoryForCwd(workspacePath) From 586e2379e269247cc31320e412049f23131c54c7 Mon Sep 17 00:00:00 2001 From: Will Li Date: Thu, 17 Jul 2025 11:45:49 -0700 Subject: [PATCH 03/13] new task spin up is working --- src/core/webview/webviewMessageHandler.ts | 25 +- src/services/rules/rulesGenerator.ts | 343 +++++------------- .../src/components/settings/RulesSettings.tsx | 8 +- webview-ui/src/i18n/locales/en/settings.json | 10 +- 4 files changed, 120 insertions(+), 266 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e0602f16a5..f389ce602a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1884,7 +1884,7 @@ export const webviewMessageHandler = async ( } break case "generateRules": - // Generate rules for the current workspace + // Generate rules for the current workspace by spawning a new task try { const workspacePath = getWorkspacePath() if (!workspacePath) { @@ -1896,20 +1896,27 @@ export const webviewMessageHandler = async ( break } - // Get the current API configuration - const { apiConfiguration } = await provider.getState() - // Import the rules generation service - const { generateRulesForWorkspace } = await import("../../services/rules/rulesGenerator") + const { createRulesGenerationTaskMessage } = await import("../../services/rules/rulesGenerator") + + // Create a comprehensive message for the rules generation task using existing analysis logic + const rulesGenerationMessage = await createRulesGenerationTaskMessage(workspacePath) - // Generate the rules with LLM support - const rulesPath = await generateRulesForWorkspace(workspacePath, apiConfiguration) + // Spawn a new task in code mode to generate the rules + await provider.initClineWithTask(rulesGenerationMessage) - // Send success message back to webview + // Send success message back to webview indicating task was created await provider.postMessageToWebview({ type: "rulesGenerationStatus", success: true, - text: rulesPath, + 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", + action: "switchTab", + tab: "chat", }) } catch (error) { // Send error message back to webview diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index fe57cfc7a4..1104c4c34b 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -1,10 +1,6 @@ import * as fs from "fs/promises" import * as path from "path" -import * as vscode from "vscode" -import type { ProviderSettings } from "@roo-code/types" import { fileExistsAtPath } from "../../utils/fs" -import { getProjectRooDirectoryForCwd } from "../roo-config/index" -import { singleCompletionHandler } from "../../utils/single-completion-handler" interface ProjectConfig { type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" @@ -60,40 +56,54 @@ async function analyzeProjectConfig(workspacePath: string): Promise { const summary: string[] = [] - // Project structure overview - summary.push("# Codebase Analysis") + summary.push("## Project Configuration Analysis") summary.push("") - summary.push(`**Project Type**: ${config.type}`) - summary.push(`**Package Manager**: ${config.packageManager || "none"}`) - summary.push("") - - // Configuration files - summary.push("## Configuration Files Found:") - const configFiles: string[] = [] - - if (await fileExistsAtPath(path.join(workspacePath, "package.json"))) { - configFiles.push("package.json") - } - if (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) { - configFiles.push("tsconfig.json") - } - if (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.js"))) { - configFiles.push(".eslintrc.js") - } - if (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.json"))) { - configFiles.push(".eslintrc.json") - } - if (await fileExistsAtPath(path.join(workspacePath, ".prettierrc"))) { - configFiles.push(".prettierrc") - } - if (await fileExistsAtPath(path.join(workspacePath, "jest.config.js"))) { - configFiles.push("jest.config.js") - } - if (await fileExistsAtPath(path.join(workspacePath, "vitest.config.ts"))) { - configFiles.push("vitest.config.ts") - } - if (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) { - configFiles.push("pyproject.toml") - } - if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { - configFiles.push("Cargo.toml") - } - if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { - configFiles.push("go.mod") - } - - configFiles.forEach((file) => summary.push(`- ${file}`)) + summary.push(`**Project Type:** ${config.type}`) + summary.push(`**Package Manager:** ${config.packageManager || "None detected"}`) summary.push("") - // Dependencies + // List key dependencies if (config.dependencies.length > 0) { - summary.push("## Key Dependencies:") - config.dependencies.slice(0, 10).forEach((dep) => summary.push(`- ${dep}`)) + summary.push("### Key Dependencies:") + const keyDeps = config.dependencies.slice(0, 10) + keyDeps.forEach((dep) => summary.push(`- ${dep}`)) + if (config.dependencies.length > 10) { + summary.push(`- ... and ${config.dependencies.length - 10} more`) + } summary.push("") } + // List dev dependencies if (config.devDependencies.length > 0) { - summary.push("## Dev Dependencies:") - config.devDependencies.slice(0, 10).forEach((dep) => summary.push(`- ${dep}`)) + summary.push("### Development Dependencies:") + const keyDevDeps = config.devDependencies.slice(0, 10) + keyDevDeps.forEach((dep) => summary.push(`- ${dep}`)) + if (config.devDependencies.length > 10) { + summary.push(`- ... and ${config.devDependencies.length - 10} more`) + } summary.push("") } - // Scripts - if (Object.keys(config.scripts).length > 0) { - summary.push("## Available Scripts:") - Object.entries(config.scripts).forEach(([name, command]) => { - summary.push(`- **${name}**: \`${command}\``) - }) + // List available scripts + const scriptKeys = Object.keys(config.scripts) + if (scriptKeys.length > 0) { + summary.push("### Available Scripts:") + scriptKeys.forEach((script) => summary.push(`- ${script}: ${config.scripts[script]}`)) summary.push("") } - // Tools detected - summary.push("## Tools Detected:") + // List detected tools + summary.push("### Detected Tools and Frameworks:") const tools: string[] = [] if (config.hasTypeScript) tools.push("TypeScript") if (config.hasESLint) tools.push("ESLint") @@ -194,6 +170,10 @@ async function generateCodebaseSummary(workspacePath: string, config: ProjectCon if (config.hasVitest) tools.push("Vitest") if (config.hasPytest) tools.push("Pytest") + if (tools.length === 0) { + tools.push("No specific tools detected") + } + tools.forEach((tool) => summary.push(`- ${tool}`)) summary.push("") @@ -222,182 +202,49 @@ async function generateCodebaseSummary(workspacePath: string, config: ProjectCon } /** - * Generates rules content using LLM analysis with fallback to deterministic approach + * Creates a comprehensive task message for rules generation that can be used with initClineWithTask */ -async function generateRulesWithLLM( - workspacePath: string, - config: ProjectConfig, - apiConfiguration?: ProviderSettings, -): Promise { - if (!apiConfiguration) { - // Fallback to deterministic generation - return generateDeterministicRules(config, workspacePath) - } +export async function createRulesGenerationTaskMessage(workspacePath: string): 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 { - const codebaseSummary = await generateCodebaseSummary(workspacePath, config) - - const prompt = `Please analyze this codebase and create a comprehensive rules file containing: - -1. Build/lint/test commands - especially for running a single test -2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc. -3. Project-specific conventions and best practices - -The file you create will be given to agentic coding agents that operate in this repository. Make it about 20 lines long and focus on the most important rules for this specific project. - -If there are existing rules files mentioned below, make sure to incorporate and improve upon them. - -Here's the codebase analysis: - -${codebaseSummary} - -Please respond with only the rules content in markdown format, starting with "# Project Rules".` - - const llmResponse = await singleCompletionHandler(apiConfiguration, prompt) - - // Validate that we got a reasonable response - if (llmResponse && llmResponse.trim().length > 100 && llmResponse.includes("# Project Rules")) { - return llmResponse.trim() - } else { - console.warn("LLM response was invalid, falling back to deterministic generation") - return generateDeterministicRules(config, workspacePath) - } + await fs.mkdir(rooRulesDir, { recursive: true }) } catch (error) { - console.error("Error generating rules with LLM, falling back to deterministic generation:", error) - return generateDeterministicRules(config, workspacePath) - } -} - -/** - * Generates rules content deterministically (fallback approach) - */ -function generateDeterministicRules(config: ProjectConfig, workspacePath: string): string { - const sections: string[] = [] - - // Header - sections.push("# Project Rules") - sections.push("") - sections.push(`Generated on: ${new Date().toISOString()}`) - sections.push(`Project type: ${config.type}`) - sections.push("") - - // Build and Development - sections.push("## Build and Development") - sections.push("") - - if (config.packageManager) { - sections.push(`- Package manager: ${config.packageManager}`) - sections.push(`- Install dependencies: \`${config.packageManager} install\``) - - if (config.scripts.build) { - sections.push(`- Build command: \`${config.packageManager} run build\``) - } - if (config.scripts.test) { - sections.push(`- Test command: \`${config.packageManager} run test\``) - } - if (config.scripts.dev || config.scripts.start) { - const devScript = config.scripts.dev || config.scripts.start - sections.push( - `- Development server: \`${config.packageManager} run ${config.scripts.dev ? "dev" : "start"}\``, - ) - } - } - - sections.push("") - - // Code Style and Linting - sections.push("## Code Style and Linting") - sections.push("") - - if (config.hasESLint) { - sections.push("- ESLint is configured for this project") - sections.push("- Run linting: `npm run lint` (if configured)") - sections.push("- Follow ESLint rules and fix any linting errors before committing") - } - - if (config.hasPrettier) { - sections.push("- Prettier is configured for code formatting") - sections.push("- Format code before committing") - sections.push("- Run formatting: `npm run format` (if configured)") - } - - if (config.hasTypeScript) { - sections.push("- TypeScript is used in this project") - sections.push("- Ensure all TypeScript errors are resolved before committing") - sections.push("- Use proper type annotations and avoid `any` types") - sections.push("- Run type checking: `npm run type-check` or `tsc --noEmit`") - } - - sections.push("") - - // Testing - sections.push("## Testing") - sections.push("") - - if (config.hasJest || config.hasVitest) { - const testFramework = config.hasVitest ? "Vitest" : "Jest" - sections.push(`- ${testFramework} is used for testing`) - sections.push("- Write tests for new features and bug fixes") - sections.push("- Ensure all tests pass before committing") - sections.push(`- Run tests: \`${config.packageManager || "npm"} run test\``) - - if (config.hasVitest) { - sections.push("- Vitest specific: Test files should use `.test.ts` or `.spec.ts` extensions") - sections.push("- The `describe`, `test`, `it` functions are globally available") - } + // Directory might already exist, which is fine } - if (config.hasPytest && config.type === "python") { - sections.push("- Pytest is used for testing") - sections.push("- Write tests in `test_*.py` or `*_test.py` files") - sections.push("- Run tests: `pytest`") - } + // 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. - sections.push("") +Your task is to: - // General Best Practices - sections.push("## General Best Practices") - sections.push("") - sections.push("- Write clear, self-documenting code") - sections.push("- Add comments for complex logic") - sections.push("- Keep functions small and focused") - sections.push("- Follow DRY (Don't Repeat Yourself) principle") - sections.push("- Handle edge cases and errors gracefully") - sections.push("- Write meaningful commit messages") - sections.push("") +1. **Analyze the project structure** - The codebase has been analyzed and here's what was found: - return sections.join("\n") -} +${codebaseSummary} -/** - * Generates rules for the workspace and saves them to a file - */ -export async function generateRulesForWorkspace( - workspacePath: string, - apiConfiguration?: ProviderSettings, -): Promise { - // Analyze the project - const config = await analyzeProjectConfig(workspacePath) +2. **Create comprehensive rules** that include: + - Build/lint/test commands (especially for running single tests) + - Code style guidelines including imports, formatting, types, naming conventions + - Error handling patterns + - Project-specific conventions and best practices + - File organization patterns - // Generate rules content using LLM with fallback - const rulesContent = await generateRulesWithLLM(workspacePath, config, apiConfiguration) +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 - // Ensure .roo/rules directory exists - const rooDir = getProjectRooDirectoryForCwd(workspacePath) - const rulesDir = path.join(rooDir, "rules") - await fs.mkdir(rulesDir, { recursive: true }) +4. **Open the generated file** in the editor for review - // Generate filename with timestamp - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5) - const rulesFileName = `generated-rules-${timestamp}.md` - const rulesPath = path.join(rulesDir, rulesFileName) +The rules should be about 20-30 lines long and focus on the most important guidelines for this specific project. Make them actionable and specific to help AI agents work effectively in this codebase. - // Write rules file - await fs.writeFile(rulesPath, rulesContent, "utf-8") +If there are existing rules files (like CLAUDE.md, .cursorrules, .cursor/rules, .github/copilot-instructions.md), incorporate and improve upon them. - // Open the file in VSCode - const doc = await vscode.workspace.openTextDocument(rulesPath) - await vscode.window.showTextDocument(doc) +Use the \`safeWriteJson\` utility from \`src/utils/safeWriteJson.ts\` for any JSON file operations to ensure atomic writes.` - return rulesPath + return taskMessage } diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index 9222476f25..01909ac80f 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -84,16 +84,14 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { {isGenerating && (

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

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

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

-

- {t("settings:rules.successDescription", { path: generationStatus.message })} -

+

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

+

{generationStatus.message}

)} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 9c0a9c6935..284429a327 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -606,13 +606,15 @@ "title": "Rules", "description": "Configure automatic rules generation for your codebase. Rules help AI agents understand your project's conventions and best practices.", "generateButton": "Generate Rules", - "generateButtonTooltip": "Analyze the codebase and generate rules automatically", - "generating": "Generating 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}}", - "error": "Failed to generate rules", - "errorDescription": "An error occurred while generating rules: {{error}}", + "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.", From 83903797ccc403e1185d447b9fc6aa4297e72c7d Mon Sep 17 00:00:00 2001 From: Will Li Date: Thu, 17 Jul 2025 13:10:30 -0700 Subject: [PATCH 04/13] 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", From 236ba2c1e71b2433ca8efecd62016d270530759e Mon Sep 17 00:00:00 2001 From: Will Li Date: Thu, 17 Jul 2025 18:26:24 -0700 Subject: [PATCH 05/13] UI changes + refactor --- .../rules/__tests__/rulesGenerator.test.ts | 100 ++++++ src/services/rules/rulesGenerator.ts | 302 ++++-------------- .../src/components/settings/RulesSettings.tsx | 261 +++++++-------- webview-ui/src/i18n/locales/en/settings.json | 9 +- 4 files changed, 306 insertions(+), 366 deletions(-) create mode 100644 src/services/rules/__tests__/rulesGenerator.test.ts diff --git a/src/services/rules/__tests__/rulesGenerator.test.ts b/src/services/rules/__tests__/rulesGenerator.test.ts new file mode 100644 index 0000000000..29d15cda5b --- /dev/null +++ b/src/services/rules/__tests__/rulesGenerator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as fs from "fs/promises" +import * as path from "path" +import { createRulesGenerationTaskMessage } from "../rulesGenerator" + +// Mock fs module +vi.mock("fs/promises", () => ({ + mkdir: vi.fn(), +})) + +describe("rulesGenerator", () => { + const mockWorkspacePath = "/test/workspace" + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("createRulesGenerationTaskMessage", () => { + it("should create directories when alwaysAllowWriteProtected is true", async () => { + await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, true) + + // Verify mkdir was called for each directory + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules"), { recursive: true }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-code"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-architect"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-debug"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-docs-extractor"), { + recursive: true, + }) + }) + + it("should NOT create directories when alwaysAllowWriteProtected is false", async () => { + await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, false) + + // Verify mkdir was NOT called + expect(fs.mkdir).not.toHaveBeenCalled() + }) + + it("should include auto-approval note in message when alwaysAllowWriteProtected is true", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, true) + + expect(message).toContain("The directory has already been created for you") + expect(message).toContain("Auto-approval for protected file writes is enabled") + }) + + it("should include manual approval note in message when alwaysAllowWriteProtected is false", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, false) + + expect(message).toContain("Create the necessary directories if they don't exist") + expect(message).toContain("You will need to approve the creation of protected directories and files") + }) + + it("should handle multiple rule types", async () => { + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general", "code", "architect"], + false, + true, + ) + + 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") + }) + + it("should include gitignore instructions when addToGitignore is true", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], true, false) + + expect(message).toContain("Add the generated files to .gitignore") + }) + + it("should include analysis steps for each rule type", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["code"], false, false) + + // Check that code-specific analysis steps are included + expect(message).toContain("Analyze package.json or equivalent files") + expect(message).toContain("Check for linting and formatting tools") + expect(message).toContain("Examine test files to understand testing patterns") + }) + + it("should include different analysis steps for different rule types", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["architect"], false, false) + + // Check that architect-specific analysis steps are included + expect(message).toContain("Analyze the overall directory structure") + expect(message).toContain("Identify architectural patterns") + expect(message).toContain("separation of concerns") + }) + }) +}) diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index 4ea649a02d..3df16a2d99 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -1,205 +1,5 @@ import * as fs from "fs/promises" import * as path from "path" -import { fileExistsAtPath } from "../../utils/fs" - -interface ProjectConfig { - type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" - hasTypeScript: boolean - hasESLint: boolean - hasPrettier: boolean - hasJest: boolean - hasVitest: boolean - hasPytest: boolean - packageManager: "npm" | "yarn" | "pnpm" | "bun" | null - dependencies: string[] - devDependencies: string[] - scripts: Record -} - -/** - * Analyzes the project configuration files to determine project type and tools - */ -async function analyzeProjectConfig(workspacePath: string): Promise { - const config: ProjectConfig = { - type: "unknown", - hasTypeScript: false, - hasESLint: false, - hasPrettier: false, - hasJest: false, - hasVitest: false, - hasPytest: false, - packageManager: null, - dependencies: [], - devDependencies: [], - scripts: {}, - } - - // Check for package.json - const packageJsonPath = path.join(workspacePath, "package.json") - if (await fileExistsAtPath(packageJsonPath)) { - try { - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")) - - // Determine package manager - if (await fileExistsAtPath(path.join(workspacePath, "yarn.lock"))) { - config.packageManager = "yarn" - } else if (await fileExistsAtPath(path.join(workspacePath, "pnpm-lock.yaml"))) { - config.packageManager = "pnpm" - } else if (await fileExistsAtPath(path.join(workspacePath, "bun.lockb"))) { - config.packageManager = "bun" - } else if (await fileExistsAtPath(path.join(workspacePath, "package-lock.json"))) { - config.packageManager = "npm" - } - - // Extract dependencies - config.dependencies = Object.keys(packageJson.dependencies || {}) - config.devDependencies = Object.keys(packageJson.devDependencies || {}) - config.scripts = packageJson.scripts || {} - - // Check for TypeScript - if ( - config.devDependencies.includes("typescript") || - config.dependencies.includes("typescript") || - (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) - ) { - config.hasTypeScript = true - config.type = "typescript" - } else { - config.type = "javascript" - } - - // Check for testing frameworks - if (config.devDependencies.includes("jest") || config.dependencies.includes("jest")) { - config.hasJest = true - } - if (config.devDependencies.includes("vitest") || config.dependencies.includes("vitest")) { - config.hasVitest = true - } - - // Check for linting/formatting - if (config.devDependencies.includes("eslint") || config.dependencies.includes("eslint")) { - config.hasESLint = true - } - if (config.devDependencies.includes("prettier") || config.dependencies.includes("prettier")) { - config.hasPrettier = true - } - } catch (error) { - console.error("Error parsing package.json:", error) - } - } - - // Check for Python project - if (await fileExistsAtPath(path.join(workspacePath, "requirements.txt"))) { - config.type = "python" - } else if (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) { - config.type = "python" - // Check for pytest - try { - const pyprojectContent = await fs.readFile(path.join(workspacePath, "pyproject.toml"), "utf-8") - if (pyprojectContent.includes("pytest")) { - config.hasPytest = true - } - } catch (error) { - console.error("Error reading pyproject.toml:", error) - } - } else if (await fileExistsAtPath(path.join(workspacePath, "setup.py"))) { - config.type = "python" - } - - // Check for other project types - if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { - config.type = "go" - } else if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { - config.type = "rust" - } else if (await fileExistsAtPath(path.join(workspacePath, "pom.xml"))) { - config.type = "java" - } - - return config -} - -/** - * Generates a summary of the codebase for LLM analysis - */ -async function generateCodebaseSummary(workspacePath: string, config: ProjectConfig): Promise { - const summary: string[] = [] - - summary.push("## Project Configuration Analysis") - summary.push("") - summary.push(`**Project Type:** ${config.type}`) - summary.push(`**Package Manager:** ${config.packageManager || "None detected"}`) - summary.push("") - - // List key dependencies - if (config.dependencies.length > 0) { - summary.push("### Key Dependencies:") - const keyDeps = config.dependencies.slice(0, 10) - keyDeps.forEach((dep) => summary.push(`- ${dep}`)) - if (config.dependencies.length > 10) { - summary.push(`- ... and ${config.dependencies.length - 10} more`) - } - summary.push("") - } - - // List dev dependencies - if (config.devDependencies.length > 0) { - summary.push("### Development Dependencies:") - const keyDevDeps = config.devDependencies.slice(0, 10) - keyDevDeps.forEach((dep) => summary.push(`- ${dep}`)) - if (config.devDependencies.length > 10) { - summary.push(`- ... and ${config.devDependencies.length - 10} more`) - } - summary.push("") - } - - // List available scripts - const scriptKeys = Object.keys(config.scripts) - if (scriptKeys.length > 0) { - summary.push("### Available Scripts:") - scriptKeys.forEach((script) => summary.push(`- ${script}: ${config.scripts[script]}`)) - summary.push("") - } - - // List detected tools - summary.push("### Detected Tools and Frameworks:") - const tools: string[] = [] - if (config.hasTypeScript) tools.push("TypeScript") - if (config.hasESLint) tools.push("ESLint") - if (config.hasPrettier) tools.push("Prettier") - if (config.hasJest) tools.push("Jest") - if (config.hasVitest) tools.push("Vitest") - if (config.hasPytest) tools.push("Pytest") - - if (tools.length === 0) { - tools.push("No specific tools detected") - } - - tools.forEach((tool) => summary.push(`- ${tool}`)) - summary.push("") - - // Check for existing rules files - const existingRulesFiles: string[] = [] - if (await fileExistsAtPath(path.join(workspacePath, "CLAUDE.md"))) { - existingRulesFiles.push("CLAUDE.md") - } - if (await fileExistsAtPath(path.join(workspacePath, ".cursorrules"))) { - existingRulesFiles.push(".cursorrules") - } - if (await fileExistsAtPath(path.join(workspacePath, ".cursor", "rules"))) { - existingRulesFiles.push(".cursor/rules") - } - if (await fileExistsAtPath(path.join(workspacePath, ".github", "copilot-instructions.md"))) { - existingRulesFiles.push(".github/copilot-instructions.md") - } - - if (existingRulesFiles.length > 0) { - summary.push("## Existing Rules Files:") - existingRulesFiles.forEach((file) => summary.push(`- ${file}`)) - summary.push("") - } - - return summary.join("\n") -} /** * Creates a comprehensive task message for rules generation that can be used with initClineWithTask @@ -210,24 +10,22 @@ export async function createRulesGenerationTaskMessage( addToGitignore: boolean, alwaysAllowWriteProtected: boolean = false, ): Promise { - // Analyze the project to get context - const config = await analyzeProjectConfig(workspacePath) - const codebaseSummary = await generateCodebaseSummary(workspacePath, config) - - // 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 + // Only create directories if auto-approve is enabled + if (alwaysAllowWriteProtected) { + 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 + } } } @@ -235,6 +33,7 @@ export async function createRulesGenerationTaskMessage( interface RuleInstruction { path: string focus: string + analysisSteps: string[] } const ruleInstructions: RuleInstruction[] = selectedRuleTypes @@ -244,26 +43,61 @@ export async function createRulesGenerationTaskMessage( 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", + analysisSteps: [ + "Examine the project structure and file organization patterns", + "Identify naming conventions for files, functions, variables, and classes", + "Look for general coding patterns and conventions used throughout the codebase", + "Check for any existing documentation or README files that describe project standards", + ], } 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", + analysisSteps: [ + "Analyze package.json or equivalent files to identify dependencies and build tools", + "Check for linting and formatting tools (ESLint, Prettier, etc.) and their configurations", + "Examine test files to understand testing patterns and frameworks used", + "Look for error handling patterns and logging strategies", + "Identify code style preferences and import/export patterns", + "Check for TypeScript usage and type definition patterns if applicable", + ], } 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", + analysisSteps: [ + "Analyze the overall directory structure and module organization", + "Identify architectural patterns (MVC, microservices, monorepo, etc.)", + "Look for separation of concerns and layering patterns", + "Check for API design patterns and service boundaries", + "Examine how different parts of the system communicate", + ], } 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", + analysisSteps: [ + "Identify logging frameworks and patterns used in the codebase", + "Look for error handling and exception patterns", + "Check for debugging tools or scripts in the project", + "Analyze test structure for debugging approaches", + "Look for monitoring or observability patterns", + ], } 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", + analysisSteps: [ + "Check for existing documentation files and their formats", + "Analyze code comments and documentation patterns", + "Look for API documentation tools or generators", + "Identify documentation structure and organization patterns", + "Check for examples or tutorials in the codebase", + ], } default: return null @@ -276,40 +110,40 @@ export async function createRulesGenerationTaskMessage( Your task is to: -1. **Analyze the project structure** - The codebase has been analyzed and here's what was found: - -${codebaseSummary} +1. **Analyze the project structure** by: +${ruleInstructions.map((rule) => ` - For ${rule.path.split("/").pop()}: ${rule.analysisSteps.join("; ")}`).join("\n")} -2. **Create comprehensive rules** that include: - - Build/lint/test commands (especially for running single tests) - - Code style guidelines including imports, formatting, types, naming conventions - - Error handling patterns - - Project-specific conventions and best practices - - File organization patterns +2. **Look for existing rule files** that might provide guidance: + - Check for CLAUDE.md, .cursorrules, .cursor/rules, or .github/copilot-instructions.md + - If found, incorporate and improve upon their 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 + - Focus: ${rule.focus}${alwaysAllowWriteProtected ? "\n - The directory has already been created for you" : "\n - Create the necessary directories if they don't exist"} - 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" : ""}`, + - 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" : "\n - Note: You will need to approve the creation of protected directories and files"}`, ) .join("\n")} -4. **Open the generated file** in the editor for review +4. **Make the rules actionable and specific** by including: + - Build/lint/test commands (especially for running single tests) + - Code style guidelines including imports, formatting, types, naming conventions + - Error handling patterns specific to this project + - Project-specific conventions and best practices + - File organization patterns -The rules should be about 20-30 lines long and focus on the most important guidelines for this specific project. Make them actionable and specific to help AI agents work effectively in this codebase. +5. **Keep rules concise** - aim for 20-30 lines per file, focusing on the most important guidelines -If there are existing rules files (like CLAUDE.md, .cursorrules, .cursor/rules, .github/copilot-instructions.md), incorporate and improve upon them. +6. **Open the generated files** in the editor for review after creation 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**: + ? `7. **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 diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index a2d3c3dcde..95e5e928ad 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 } from "lucide-react" +import { FileText, Loader2, AlertTriangle, Info, Sparkles } from "lucide-react" import { Button } from "@/components/ui/button" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" @@ -156,7 +156,7 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { return (

- +
{t("settings:rules.title")}
@@ -164,132 +164,135 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => {
-
-

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

- - {/* Recommendation box */} -
- -
- {t("settings:rules.autoApproveRecommendation")} +
+ {/* Magic Rules Generation subsection */} +
+
+
+ +
{t("settings:rules.magicGeneration.title")}
+
+
+ {t("settings:rules.magicGeneration.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} +
+

{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}
  • - ))} -
+ ))}
- )} -
-
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index fc58c4f659..be21a3ff3a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -604,7 +604,11 @@ }, "rules": { "title": "Rules", - "description": "Configure automatic rules generation for your codebase. Rules help AI agents understand your project's conventions and best practices.", + "description": "Rules help AI agents understand your project's conventions and best practices!", + "magicGeneration": { + "title": "Magic Rules Generation", + "description": "Automatically analyze your codebase and generate comprehensive rules tailored to your project's specific needs." + }, "generateButton": "Generate Rules", "generateButtonTooltip": "Create a new task to analyze the codebase and generate rules automatically", "generating": "Creating task...", @@ -649,9 +653,8 @@ "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", + "autoApproveProtectedDescription": "Allow Roo to make and 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": { From 3890db6f606329a0705c81f2efd1809b17cfdbea Mon Sep 17 00:00:00 2001 From: Will Li Date: Fri, 18 Jul 2025 00:09:43 -0700 Subject: [PATCH 06/13] fix the settings save edge case --- .../settings/ExperimentalSettings.tsx | 4 +- .../src/components/settings/RulesSettings.tsx | 57 +++++++++++-------- .../src/components/settings/SettingsView.tsx | 6 +- webview-ui/src/i18n/locales/en/settings.json | 1 + 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index ea59ab89f8..9bdae6cfa7 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -17,11 +17,13 @@ import { RulesSettings } from "./RulesSettings" type ExperimentalSettingsProps = HTMLAttributes & { experiments: Experiments setExperimentEnabled: SetExperimentEnabled + hasUnsavedChanges?: boolean } export const ExperimentalSettings = ({ experiments, setExperimentEnabled, + hasUnsavedChanges, className, ...props }: ExperimentalSettingsProps) => { @@ -68,7 +70,7 @@ export const ExperimentalSettings = ({ })} - +
) } diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index 95e5e928ad..c4d61ca704 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -5,12 +5,14 @@ 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" -type RulesSettingsProps = HTMLAttributes +type RulesSettingsProps = HTMLAttributes & { + hasUnsavedChanges?: boolean +} interface RuleType { id: string @@ -20,7 +22,7 @@ interface RuleType { exists?: boolean } -export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { +export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesSettingsProps) => { const { t } = useAppTranslation() const [isGenerating, setIsGenerating] = useState(false) const [generationStatus, setGenerationStatus] = useState<{ @@ -287,25 +289,33 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { - + + + + +
{isGenerating && ( @@ -316,8 +326,7 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { {generationStatus.type === "success" && (
-

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

-

{generationStatus.message}

+

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

)} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index cf9e779cbd..8314787aae 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -689,7 +689,11 @@ const SettingsView = forwardRef(({ onDone, t {/* Experimental Section */} {activeTab === "experimental" && ( - + )} {/* Language Section */} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index be21a3ff3a..b9848a867a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -626,6 +626,7 @@ "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", "types": { "general": { "label": "General Rules", From 775c321e7f0d517293febf34d007f20b6f60cba4 Mon Sep 17 00:00:00 2001 From: Will Li Date: Fri, 18 Jul 2025 01:28:36 -0700 Subject: [PATCH 07/13] added small repo warning + custom instructions --- src/core/webview/webviewMessageHandler.ts | 26 +++++++- src/services/rules/rulesGenerator.ts | 10 ++- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 2 + .../src/components/settings/RulesSettings.tsx | 66 +++++++++++++++++-- webview-ui/src/i18n/locales/en/settings.json | 7 +- 6 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 11e208bbed..6c300bcd56 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1904,6 +1904,8 @@ export const webviewMessageHandler = async ( const addToGitignore = message.addToGitignore || false const alwaysAllowWriteProtected = message.alwaysAllowWriteProtected || false const apiConfigName = message.apiConfigName + const includeCustomRules = message.includeCustomRules || false + const customRulesText = message.customRulesText || "" // Save current API config to restore later const currentApiConfig = getGlobalState("currentApiConfigName") @@ -1920,6 +1922,8 @@ export const webviewMessageHandler = async ( selectedRuleTypes, addToGitignore, alwaysAllowWriteProtected, + includeCustomRules, + customRulesText, ) // Spawn a new task in code mode to generate the rules @@ -1954,7 +1958,7 @@ export const webviewMessageHandler = async ( } break case "checkExistingRuleFiles": - // Check which rule files already exist + // Check which rule files already exist and count source files try { const workspacePath = getWorkspacePath() if (!workspacePath) { @@ -1963,6 +1967,7 @@ export const webviewMessageHandler = async ( const { fileExistsAtPath } = await import("../../utils/fs") const path = await import("path") + const fs = await import("fs/promises") const ruleTypeToPath: Record = { general: path.join(workspacePath, ".roo", "rules", "coding-standards.md"), @@ -1984,9 +1989,28 @@ export const webviewMessageHandler = async ( } } + // Count all files in the workspace + let sourceFileCount = 0 + try { + // Use VS Code API to count all files + const vscode = await import("vscode") + + // Find all files (excluding common non-project files) + const pattern = "**/*" + const excludePattern = + "**/node_modules/**,**/.git/**,**/dist/**,**/build/**,**/.next/**,**/.nuxt/**,**/coverage/**,**/.cache/**" + + const files = await vscode.workspace.findFiles(pattern, excludePattern) + sourceFileCount = files.length + } catch (error) { + // If counting fails, set to -1 to indicate unknown + sourceFileCount = -1 + } + await provider.postMessageToWebview({ type: "existingRuleFiles", files: existingFiles, + sourceFileCount, }) } catch (error) { // Silently fail - not critical diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index 3df16a2d99..bdcc82acbf 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -9,6 +9,8 @@ export async function createRulesGenerationTaskMessage( selectedRuleTypes: string[], addToGitignore: boolean, alwaysAllowWriteProtected: boolean = false, + includeCustomRules: boolean = false, + customRulesText: string = "", ): Promise { // Only create directories if auto-approve is enabled if (alwaysAllowWriteProtected) { @@ -135,7 +137,7 @@ ${ruleInstructions - Project-specific conventions and best practices - File organization patterns -5. **Keep rules concise** - aim for 20-30 lines per file, focusing on the most important guidelines +5. **Keep rules concise** - aim for 20 lines per file, focusing on the most important guidelines 6. **Open the generated files** in the editor for review after creation @@ -149,6 +151,12 @@ ${ - If .gitignore doesn't exist, create it - If the entries already exist in .gitignore, don't duplicate them` : "" +} + +${ + includeCustomRules && customRulesText + ? `\n**Additional rules from User to add to the rules file:**\n${customRulesText}` + : "" }` return taskMessage diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 35add00e37..0b2d122e77 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -110,6 +110,7 @@ export interface ExtensionMessage { text?: string payload?: any // Add a generic payload for now, can refine later files?: string[] // For existingRuleFiles + sourceFileCount?: number // For existingRuleFiles to show warning for small repos action?: | "chatButtonClicked" | "mcpButtonClicked" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 6d33dd142f..633fc11608 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -241,6 +241,8 @@ export interface WebviewMessage { addToGitignore?: boolean // For generateRules alwaysAllowWriteProtected?: boolean // For generateRules apiConfigName?: string // For generateRules + includeCustomRules?: boolean // For generateRules + customRulesText?: string // For generateRules files?: string[] // For existingRuleFiles response codeIndexSettings?: { // Global state settings diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index c4d61ca704..2b65b51389 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -6,6 +6,7 @@ import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import { useExtensionState } from "@/context/ExtensionStateContext" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@/components/ui" +import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" @@ -29,10 +30,13 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS type: "success" | "error" | null message: string }>({ type: null, message: "" }) - const [addToGitignore, setAddToGitignore] = useState(false) + const [addToGitignore, setAddToGitignore] = useState(true) const [_existingFiles, setExistingFiles] = useState([]) - const [alwaysAllowWriteProtected, setAlwaysAllowWriteProtected] = useState(false) + const [alwaysAllowWriteProtected, setAlwaysAllowWriteProtected] = useState(true) const [selectedApiConfig, setSelectedApiConfig] = useState("") + const [includeCustomRules, setIncludeCustomRules] = useState(false) + const [customRulesText, setCustomRulesText] = useState("") + const [sourceFileCount, setSourceFileCount] = useState(null) const { listApiConfigMeta, currentApiConfigName } = useExtensionState() @@ -118,6 +122,10 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS exists: message.files?.includes(rule.id) || false, })), ) + // Set source file count if provided + if (message.sourceFileCount !== undefined) { + setSourceFileCount(message.sourceFileCount) + } } else if (message.type === "state") { // Update alwaysAllowWriteProtected from the extension state if (message.state?.alwaysAllowWriteProtected !== undefined) { @@ -150,6 +158,8 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS addToGitignore, alwaysAllowWriteProtected, apiConfigName: selectedApiConfig, + includeCustomRules, + customRulesText: includeCustomRules ? customRulesText : "", }) } @@ -221,6 +231,15 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS
+ {/* Small repository warning */} + {sourceFileCount !== null && sourceFileCount > 0 && sourceFileCount < 20 && ( +
+ +
+ {t("settings:rules.smallRepoWarning", { count: sourceFileCount })} +
+
+ )} {hasExistingFiles && (
@@ -235,7 +254,7 @@ export const RulesSettings = ({ className, hasUnsavedChanges, ...props }: RulesS
)} -
+
-
+
+
+ + + {includeCustomRules && ( +
+ { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + setCustomRulesText(value) + }} + placeholder={t("settings:rules.customRulesPlaceholder")} + rows={6} + className="w-full" + /> +
+ {t("settings:rules.customRulesHint")} +
+
+ )} +
+
-
-
-
-