From e09afaa4836569cc3e0a862172534ac1c5a12628 Mon Sep 17 00:00:00 2001 From: Will Li Date: Wed, 16 Jul 2025 08:40:14 -0700 Subject: [PATCH] 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."