initial working

This commit is contained in:
Will Li 2025-07-16 08:40:14 -07:00
parent d513b9c0e1
commit e09afaa483
7 changed files with 458 additions and 0 deletions

View file

@ -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"), {

View file

@ -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<string, string>
}
/**
* Analyzes the project configuration files to determine project type and tools
*/
async function analyzeProjectConfig(workspacePath: string): Promise<ProjectConfig> {
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<string> {
// 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
}

View file

@ -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?:

View file

@ -194,6 +194,7 @@ export interface WebviewMessage {
| "checkRulesDirectoryResult"
| "saveCodeIndexSettingsAtomic"
| "requestCodeIndexSecretStatus"
| "generateRules"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"

View file

@ -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<HTMLDivElement> & {
experiments: Experiments
@ -66,6 +67,8 @@ export const ExperimentalSettings = ({
)
})}
</Section>
<RulesSettings className="mt-6" />
</div>
)
}

View file

@ -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<HTMLDivElement>
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 (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<SectionHeader>
<div className="flex items-center gap-2">
<FileText className="w-4" />
<div>{t("settings:rules.title")}</div>
</div>
</SectionHeader>
<Section>
<div className="space-y-4">
<p className="text-vscode-descriptionForeground text-sm">{t("settings:rules.description")}</p>
<div className="flex flex-col gap-3">
<Button
onClick={handleGenerateRules}
disabled={isGenerating}
variant="default"
size="default"
className="w-fit"
title={t("settings:rules.generateButtonTooltip")}>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("settings:rules.generating")}
</>
) : (
t("settings:rules.generateButton")
)}
</Button>
{isGenerating && (
<p className="text-vscode-descriptionForeground text-sm">
{t("settings:rules.generatingDescription")}
</p>
)}
{generationStatus.type === "success" && (
<div className="text-vscode-testing-iconPassed text-sm">
<p>{t("settings:rules.success")}</p>
<p className="text-vscode-descriptionForeground">
{t("settings:rules.successDescription", { path: generationStatus.message })}
</p>
</div>
)}
{generationStatus.type === "error" && (
<div className="text-vscode-testing-iconFailed text-sm">
<p>{t("settings:rules.error")}</p>
<p className="text-vscode-descriptionForeground">
{t("settings:rules.errorDescription", { error: generationStatus.message })}
</p>
</div>
)}
</div>
</div>
</Section>
</div>
)
}

View file

@ -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."