basically working

This commit is contained in:
Will Li 2025-07-17 13:10:30 -07:00
parent 586e2379e2
commit 83903797cc
6 changed files with 433 additions and 32 deletions

View file

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

View file

@ -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<string> {
export async function createRulesGenerationTaskMessage(
workspacePath: string,
selectedRuleTypes: string[],
addToGitignore: boolean,
alwaysAllowWriteProtected: boolean = false,
): Promise<string> {
// 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
}

View file

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

View file

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

View file

@ -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<HTMLDivElement>
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<string[]>([])
const [alwaysAllowWriteProtected, setAlwaysAllowWriteProtected] = useState(false)
const [selectedApiConfig, setSelectedApiConfig] = useState<string>("")
const { listApiConfigMeta, currentApiConfigName } = useExtensionState()
const [ruleTypes, setRuleTypes] = useState<RuleType[]>([
{
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 (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<SectionHeader>
@ -64,23 +167,144 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => {
<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>
{/* Recommendation box */}
<div className="flex items-start gap-2 p-3 bg-vscode-inputValidation-infoBackground border border-vscode-inputValidation-infoBorder rounded-md">
<Info className="w-4 h-4 text-vscode-inputValidation-infoForeground mt-0.5 flex-shrink-0" />
<div className="text-sm text-vscode-inputValidation-infoForeground">
{t("settings:rules.autoApproveRecommendation")}
</div>
</div>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium mb-3">{t("settings:rules.selectTypes")}</h4>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
{ruleTypes.map((ruleType) => (
<div
key={ruleType.id}
onClick={() => 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",
)}>
<div className="flex-1">
<div className="text-sm font-medium flex items-center gap-1">
{ruleType.label}
{ruleType.exists && (
<span
className="text-vscode-testing-iconQueued"
title={t("settings:rules.fileExists")}>
</span>
)}
</div>
<div className="text-xs text-vscode-descriptionForeground mt-1">
{ruleType.description}
</div>
</div>
</div>
))}
</div>
</div>
{hasExistingFiles && (
<div className="flex items-start gap-2 p-3 bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder rounded-md">
<AlertTriangle className="w-4 h-4 text-vscode-inputValidation-warningForeground mt-0.5 flex-shrink-0" />
<div className="text-sm text-vscode-inputValidation-warningForeground">
<div>{t("settings:rules.overwriteWarning")}</div>
<ul className="mt-1 ml-4 list-disc">
{existingRules.map((rule) => (
<li key={rule.id}>{rule.label}</li>
))}
</ul>
</div>
</div>
)}
<div className="border-t border-vscode-panel-border pt-4">
<label className="flex items-center gap-2 cursor-pointer hover:opacity-80">
<input
type="checkbox"
checked={addToGitignore}
onChange={(e) => setAddToGitignore(e.target.checked)}
/>
<div>
<div className="text-sm font-medium">{t("settings:rules.addToGitignore")}</div>
<div className="text-xs text-vscode-descriptionForeground">
{t("settings:rules.addToGitignoreDescription")}
</div>
</div>
</label>
</div>
<div className="border-t border-vscode-panel-border pt-4">
<label className="flex items-center gap-2 cursor-pointer hover:opacity-80">
<input
type="checkbox"
checked={alwaysAllowWriteProtected}
onChange={(e) => {
setAlwaysAllowWriteProtected(e.target.checked)
vscode.postMessage({
type: "alwaysAllowWriteProtected",
bool: e.target.checked,
})
}}
/>
<div>
<div className="text-sm font-medium">
{t("settings:rules.autoApproveProtected")}
</div>
<div className="text-xs text-vscode-descriptionForeground">
{t("settings:rules.autoApproveProtectedDescription")}
</div>
</div>
</label>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="w-48">
<label className="text-sm font-medium mb-2 block">
{t("settings:rules.apiConfigLabel")}
</label>
<Select value={selectedApiConfig} onValueChange={setSelectedApiConfig}>
<SelectTrigger>
<SelectValue placeholder={t("settings:rules.selectApiConfig")} />
</SelectTrigger>
<SelectContent>
{(listApiConfigMeta || []).map((config) => (
<SelectItem key={config.id} value={config.name}>
{config.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
onClick={handleGenerateRules}
disabled={isGenerating || !selectedApiConfig}
variant="default"
size="default"
className="mt-6"
title={t("settings:rules.generateButtonTooltip")}>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("settings:rules.generating")}
</>
) : (
<>
<FileText className="mr-2 h-4 w-4" />
{t("settings:rules.generateButton")}
</>
)}
</Button>
</div>
</div>
{isGenerating && (
<p className="text-vscode-descriptionForeground text-sm">

View file

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