From d6d5670e389ed739883d756e780f37f89656503f Mon Sep 17 00:00:00 2001 From: Yu SERIZAWA Date: Sun, 6 Apr 2025 03:55:07 +0900 Subject: [PATCH] feat: Add support for .roorules configuration files (#2309) * feat: Add support for .roorules configuration files - Add .roorules to the list of supported rule files - Improve mode-specific rule file handling to prioritize .roorules over .clinerules - Update file loading logic to track which rule file was successfully loaded - Add test cases for .roorules functionality This change maintains backward compatibility with existing .clinerules while introducing support for the new .roorules format as the preferred configuration method. * refactor: simplify rule file loading by removing unused rules - Removed .cursorrules and .windsurfrules from the list of rule files in loadRuleFiles function. * refactor: update loadRuleFiles to return single rule file content - Modified loadRuleFiles function to return content from the first available rule file instead of combining multiple rule files. - Updated tests to reflect the new behavior of loading only the .roorules file content when available, ensuring clarity in rule file handling. * fix: update prompts for deprecated rule file references - Updated prompts in the English locale to reflect the deprecation of .clinerules in favor of .roorules. - Added notes in the prompts indicating that .clinerules will stop working soon, ensuring users are aware of the upcoming changes. * Translations * Update links * Revert README changes * Add missing spans around links --------- Co-authored-by: Matt Rubens --- .../__tests__/custom-instructions.test.ts | 29 +++++++------------ .../prompts/sections/custom-instructions.ts | 24 ++++++++++----- .../src/components/history/HistoryView.tsx | 7 +---- .../src/components/prompts/PromptsView.tsx | 12 ++++---- .../__tests__/ApiConfigManager.test.tsx | 12 ++++---- webview-ui/src/i18n/locales/ca/prompts.json | 4 +-- webview-ui/src/i18n/locales/de/prompts.json | 4 +-- webview-ui/src/i18n/locales/en/prompts.json | 4 +-- webview-ui/src/i18n/locales/es/prompts.json | 4 +-- webview-ui/src/i18n/locales/fr/prompts.json | 4 +-- webview-ui/src/i18n/locales/hi/prompts.json | 4 +-- webview-ui/src/i18n/locales/it/prompts.json | 4 +-- webview-ui/src/i18n/locales/ja/prompts.json | 4 +-- webview-ui/src/i18n/locales/ko/prompts.json | 4 +-- webview-ui/src/i18n/locales/pl/prompts.json | 4 +-- .../src/i18n/locales/pt-BR/prompts.json | 4 +-- webview-ui/src/i18n/locales/tr/prompts.json | 4 +-- webview-ui/src/i18n/locales/vi/prompts.json | 4 +-- .../src/i18n/locales/zh-CN/prompts.json | 4 +-- .../src/i18n/locales/zh-TW/prompts.json | 4 +-- 20 files changed, 69 insertions(+), 75 deletions(-) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.test.ts b/src/core/prompts/sections/__tests__/custom-instructions.test.ts index 4dbb51c845..80762dcac3 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.test.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.test.ts @@ -14,11 +14,7 @@ describe("loadRuleFiles", () => { mockedFs.readFile.mockResolvedValue(" content with spaces ") const result = await loadRuleFiles("/fake/path") expect(mockedFs.readFile).toHaveBeenCalled() - expect(result).toBe( - "\n# Rules from .clinerules:\ncontent with spaces\n" + - "\n# Rules from .cursorrules:\ncontent with spaces\n" + - "\n# Rules from .windsurfrules:\ncontent with spaces\n", - ) + expect(result).toBe("\n# Rules from .roorules:\ncontent with spaces\n") }) it("should handle ENOENT error", async () => { @@ -49,22 +45,19 @@ describe("loadRuleFiles", () => { jest.clearAllMocks() }) - it("should combine content from multiple rule files when they exist", async () => { + it("should not combine content from multiple rule files when they exist", async () => { mockedFs.readFile.mockImplementation(((filePath: string | Buffer | URL | number) => { + if (filePath.toString().endsWith(".roorules")) { + return Promise.resolve("roo rules content") + } if (filePath.toString().endsWith(".clinerules")) { return Promise.resolve("cline rules content") } - if (filePath.toString().endsWith(".cursorrules")) { - return Promise.resolve("cursor rules content") - } return Promise.reject({ code: "ENOENT" }) }) as any) const result = await loadRuleFiles("/fake/path") - expect(result).toBe( - "\n# Rules from .clinerules:\ncline rules content\n" + - "\n# Rules from .cursorrules:\ncursor rules content\n", - ) + expect(result).toBe("\n# Rules from .roorules:\nroo rules content\n") }) it("should handle when no rule files exist", async () => { @@ -86,17 +79,17 @@ describe("loadRuleFiles", () => { it("should skip directories with same name as rule files", async () => { mockedFs.readFile.mockImplementation(((filePath: string | Buffer | URL | number) => { - if (filePath.toString().endsWith(".clinerules")) { + if (filePath.toString().endsWith(".roorules")) { return Promise.reject({ code: "EISDIR" }) } - if (filePath.toString().endsWith(".cursorrules")) { - return Promise.resolve("cursor rules content") + if (filePath.toString().endsWith(".clinerules")) { + return Promise.reject({ code: "EISDIR" }) } return Promise.reject({ code: "ENOENT" }) }) as any) const result = await loadRuleFiles("/fake/path") - expect(result).toBe("\n# Rules from .cursorrules:\ncursor rules content\n") + expect(result).toBe("") }) }) @@ -121,7 +114,7 @@ describe("addCustomInstructions", () => { expect(result).toContain("(es)") // Check for language code in parentheses expect(result).toContain("Global Instructions:\nglobal instructions") expect(result).toContain("Mode-specific Instructions:\nmode instructions") - expect(result).toContain("Rules from .clinerules-test-mode:\nmode specific rules") + expect(result).toContain("Rules from .roorules-test-mode:\nmode specific rules") }) it("should return empty string when no instructions provided", async () => { diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index bf5b009bc2..f6b4676428 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -17,17 +17,16 @@ async function safeReadFile(filePath: string): Promise { } export async function loadRuleFiles(cwd: string): Promise { - const ruleFiles = [".clinerules", ".cursorrules", ".windsurfrules"] - let combinedRules = "" + const ruleFiles = [".roorules", ".clinerules"] for (const file of ruleFiles) { const content = await safeReadFile(path.join(cwd, file)) if (content) { - combinedRules += `\n# Rules from ${file}:\n${content}\n` + return `\n# Rules from ${file}:\n${content}\n` } } - return combinedRules + return "" } export async function addCustomInstructions( @@ -41,9 +40,19 @@ export async function addCustomInstructions( // Load mode-specific rules if mode is provided let modeRuleContent = "" + let usedRuleFile = "" if (mode) { - const modeRuleFile = `.clinerules-${mode}` - modeRuleContent = await safeReadFile(path.join(cwd, modeRuleFile)) + const rooModeRuleFile = `.roorules-${mode}` + modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile)) + if (modeRuleContent) { + usedRuleFile = rooModeRuleFile + } else { + const clineModeRuleFile = `.clinerules-${mode}` + modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile)) + if (modeRuleContent) { + usedRuleFile = clineModeRuleFile + } + } } // Add language preference if provided @@ -69,8 +78,7 @@ export async function addCustomInstructions( // Add mode-specific rules first if they exist if (modeRuleContent && modeRuleContent.trim()) { - const modeRuleFile = `.clinerules-${mode}` - rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`) + rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`) } if (options.rooIgnoreInstructions) { diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index a1bbdb556f..c74d6b0d96 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -4,12 +4,7 @@ import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog" import prettyBytes from "pretty-bytes" import { Virtuoso } from "react-virtuoso" -import { - VSCodeTextField, - VSCodeRadioGroup, - VSCodeRadio, - VSCodeCheckbox, -} from "@vscode/webview-ui-toolkit/react" +import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { vscode } from "@/utils/vscode" import { formatLargeNumber, formatDate } from "@/utils/format" diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 15044fb2f8..24ba4ad67c 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -618,10 +618,10 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{t("prompts:tools.title")}
{findModeBySlug(mode, customModes) && (