mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
initial working
This commit is contained in:
parent
d513b9c0e1
commit
e09afaa483
7 changed files with 458 additions and 0 deletions
|
|
@ -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"), {
|
||||
|
|
|
|||
289
src/services/rules/rulesGenerator.ts
Normal file
289
src/services/rules/rulesGenerator.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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?:
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ export interface WebviewMessage {
|
|||
| "checkRulesDirectoryResult"
|
||||
| "saveCodeIndexSettingsAtomic"
|
||||
| "requestCodeIndexSecretStatus"
|
||||
| "generateRules"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
113
webview-ui/src/components/settings/RulesSettings.tsx
Normal file
113
webview-ui/src/components/settings/RulesSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue