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")} +
+
+ )} +
+