Skip to content

Commit d6d5670

Browse files
upamunemrubens
andauthored
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 <[email protected]>
1 parent 8fe70e3 commit d6d5670

File tree

20 files changed

+69
-75
lines changed

20 files changed

+69
-75
lines changed

src/core/prompts/sections/__tests__/custom-instructions.test.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ describe("loadRuleFiles", () => {
1414
mockedFs.readFile.mockResolvedValue(" content with spaces ")
1515
const result = await loadRuleFiles("/fake/path")
1616
expect(mockedFs.readFile).toHaveBeenCalled()
17-
expect(result).toBe(
18-
"\n# Rules from .clinerules:\ncontent with spaces\n" +
19-
"\n# Rules from .cursorrules:\ncontent with spaces\n" +
20-
"\n# Rules from .windsurfrules:\ncontent with spaces\n",
21-
)
17+
expect(result).toBe("\n# Rules from .roorules:\ncontent with spaces\n")
2218
})
2319

2420
it("should handle ENOENT error", async () => {
@@ -49,22 +45,19 @@ describe("loadRuleFiles", () => {
4945
jest.clearAllMocks()
5046
})
5147

52-
it("should combine content from multiple rule files when they exist", async () => {
48+
it("should not combine content from multiple rule files when they exist", async () => {
5349
mockedFs.readFile.mockImplementation(((filePath: string | Buffer | URL | number) => {
50+
if (filePath.toString().endsWith(".roorules")) {
51+
return Promise.resolve("roo rules content")
52+
}
5453
if (filePath.toString().endsWith(".clinerules")) {
5554
return Promise.resolve("cline rules content")
5655
}
57-
if (filePath.toString().endsWith(".cursorrules")) {
58-
return Promise.resolve("cursor rules content")
59-
}
6056
return Promise.reject({ code: "ENOENT" })
6157
}) as any)
6258

6359
const result = await loadRuleFiles("/fake/path")
64-
expect(result).toBe(
65-
"\n# Rules from .clinerules:\ncline rules content\n" +
66-
"\n# Rules from .cursorrules:\ncursor rules content\n",
67-
)
60+
expect(result).toBe("\n# Rules from .roorules:\nroo rules content\n")
6861
})
6962

7063
it("should handle when no rule files exist", async () => {
@@ -86,17 +79,17 @@ describe("loadRuleFiles", () => {
8679

8780
it("should skip directories with same name as rule files", async () => {
8881
mockedFs.readFile.mockImplementation(((filePath: string | Buffer | URL | number) => {
89-
if (filePath.toString().endsWith(".clinerules")) {
82+
if (filePath.toString().endsWith(".roorules")) {
9083
return Promise.reject({ code: "EISDIR" })
9184
}
92-
if (filePath.toString().endsWith(".cursorrules")) {
93-
return Promise.resolve("cursor rules content")
85+
if (filePath.toString().endsWith(".clinerules")) {
86+
return Promise.reject({ code: "EISDIR" })
9487
}
9588
return Promise.reject({ code: "ENOENT" })
9689
}) as any)
9790

9891
const result = await loadRuleFiles("/fake/path")
99-
expect(result).toBe("\n# Rules from .cursorrules:\ncursor rules content\n")
92+
expect(result).toBe("")
10093
})
10194
})
10295

@@ -121,7 +114,7 @@ describe("addCustomInstructions", () => {
121114
expect(result).toContain("(es)") // Check for language code in parentheses
122115
expect(result).toContain("Global Instructions:\nglobal instructions")
123116
expect(result).toContain("Mode-specific Instructions:\nmode instructions")
124-
expect(result).toContain("Rules from .clinerules-test-mode:\nmode specific rules")
117+
expect(result).toContain("Rules from .roorules-test-mode:\nmode specific rules")
125118
})
126119

127120
it("should return empty string when no instructions provided", async () => {

src/core/prompts/sections/custom-instructions.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,16 @@ async function safeReadFile(filePath: string): Promise<string> {
1717
}
1818

1919
export async function loadRuleFiles(cwd: string): Promise<string> {
20-
const ruleFiles = [".clinerules", ".cursorrules", ".windsurfrules"]
21-
let combinedRules = ""
20+
const ruleFiles = [".roorules", ".clinerules"]
2221

2322
for (const file of ruleFiles) {
2423
const content = await safeReadFile(path.join(cwd, file))
2524
if (content) {
26-
combinedRules += `\n# Rules from ${file}:\n${content}\n`
25+
return `\n# Rules from ${file}:\n${content}\n`
2726
}
2827
}
2928

30-
return combinedRules
29+
return ""
3130
}
3231

3332
export async function addCustomInstructions(
@@ -41,9 +40,19 @@ export async function addCustomInstructions(
4140

4241
// Load mode-specific rules if mode is provided
4342
let modeRuleContent = ""
43+
let usedRuleFile = ""
4444
if (mode) {
45-
const modeRuleFile = `.clinerules-${mode}`
46-
modeRuleContent = await safeReadFile(path.join(cwd, modeRuleFile))
45+
const rooModeRuleFile = `.roorules-${mode}`
46+
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
47+
if (modeRuleContent) {
48+
usedRuleFile = rooModeRuleFile
49+
} else {
50+
const clineModeRuleFile = `.clinerules-${mode}`
51+
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
52+
if (modeRuleContent) {
53+
usedRuleFile = clineModeRuleFile
54+
}
55+
}
4756
}
4857

4958
// Add language preference if provided
@@ -69,8 +78,7 @@ export async function addCustomInstructions(
6978

7079
// Add mode-specific rules first if they exist
7180
if (modeRuleContent && modeRuleContent.trim()) {
72-
const modeRuleFile = `.clinerules-${mode}`
73-
rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`)
81+
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
7482
}
7583

7684
if (options.rooIgnoreInstructions) {

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog"
44
import prettyBytes from "pretty-bytes"
55
import { Virtuoso } from "react-virtuoso"
66

7-
import {
8-
VSCodeTextField,
9-
VSCodeRadioGroup,
10-
VSCodeRadio,
11-
VSCodeCheckbox,
12-
} from "@vscode/webview-ui-toolkit/react"
7+
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
138

149
import { vscode } from "@/utils/vscode"
1510
import { formatLargeNumber, formatDate } from "@/utils/format"

webview-ui/src/components/prompts/PromptsView.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,10 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
618618
<div className="font-bold">{t("prompts:tools.title")}</div>
619619
{findModeBySlug(mode, customModes) && (
620620
<Button
621-
variant="ghost"
622-
size="icon"
623-
onClick={() => setIsToolsEditMode(!isToolsEditMode)}
624-
title={
621+
variant="ghost"
622+
size="icon"
623+
onClick={() => setIsToolsEditMode(!isToolsEditMode)}
624+
title={
625625
isToolsEditMode
626626
? t("prompts:tools.doneEditing")
627627
: t("prompts:tools.editTools")
@@ -798,7 +798,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
798798
// Open or create an empty file
799799
vscode.postMessage({
800800
type: "openFile",
801-
text: `./.clinerules-${currentMode.slug}`,
801+
text: `./.roorules-${currentMode.slug}`,
802802
values: {
803803
create: true,
804804
content: "",
@@ -935,7 +935,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
935935
onClick={() =>
936936
vscode.postMessage({
937937
type: "openFile",
938-
text: "./.clinerules",
938+
text: "./.roorules",
939939
values: {
940940
create: true,
941941
content: "",

webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,18 +246,16 @@ describe("ApiConfigManager", () => {
246246
// Click the select component to open the dropdown
247247
const selectButton = screen.getByTestId("select-component")
248248
fireEvent.click(selectButton)
249-
249+
250250
// Find all command items and click the one with "Another Config"
251-
const commandItems = document.querySelectorAll('.command-item')
251+
const commandItems = document.querySelectorAll(".command-item")
252252
// Find the item with "Another Config" text
253-
const anotherConfigItem = Array.from(commandItems).find(
254-
item => item.textContent?.includes("Another Config")
255-
)
256-
253+
const anotherConfigItem = Array.from(commandItems).find((item) => item.textContent?.includes("Another Config"))
254+
257255
if (!anotherConfigItem) {
258256
throw new Error("Could not find 'Another Config' option")
259257
}
260-
258+
261259
fireEvent.click(anotherConfigItem)
262260

263261
expect(mockOnSelectConfig).toHaveBeenCalledWith("Another Config")

webview-ui/src/i18n/locales/ca/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instruccions personalitzades específiques del mode (opcional)",
3737
"resetToDefault": "Restablir a valors predeterminats",
3838
"description": "Afegiu directrius de comportament específiques per al mode {{modeName}}.",
39-
"loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de <span>.clinerules-{{slug}}</span> al vostre espai de treball."
39+
"loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de <span>.roorules-{{slug}}</span> al vostre espai de treball (.clinerules-{{slug}} està obsolet i deixarà de funcionar aviat)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instruccions personalitzades per a tots els modes",
4343
"description": "Aquestes instruccions s'apliquen a tots els modes. Proporcionen un conjunt bàsic de comportaments que es poden millorar amb instruccions específiques de cada mode a continuació.\nSi voleu que Roo pensi i parli en un idioma diferent al de la visualització del vostre editor ({{language}}), podeu especificar-ho aquí.",
44-
"loadFromFile": "Les instruccions també es poden carregar des de <span>.clinerules</span> al vostre espai de treball."
44+
"loadFromFile": "Les instruccions també es poden carregar des de <span>.roorules</span> al vostre espai de treball (.clinerules està obsolet i deixarà de funcionar aviat)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Previsualització del prompt del sistema",

webview-ui/src/i18n/locales/de/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Modusspezifische benutzerdefinierte Anweisungen (optional)",
3737
"resetToDefault": "Auf Standardwerte zurücksetzen",
3838
"description": "Fügen Sie verhaltensspezifische Richtlinien für den Modus {{modeName}} hinzu.",
39-
"loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus <span>.clinerules-{{slug}}</span> in deinem Arbeitsbereich geladen werden."
39+
"loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus <span>.roorules-{{slug}}</span> in deinem Arbeitsbereich geladen werden (.clinerules-{{slug}} ist veraltet und wird bald nicht mehr funktionieren)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Benutzerdefinierte Anweisungen für alle Modi",
4343
"description": "Diese Anweisungen gelten für alle Modi. Sie bieten einen grundlegenden Satz von Verhaltensweisen, die durch modusspezifische Anweisungen unten erweitert werden können.\nWenn du möchtest, dass Roo in einer anderen Sprache als deiner Editor-Anzeigesprache ({{language}}) denkt und spricht, kannst du das hier angeben.",
44-
"loadFromFile": "Anweisungen können auch aus <span>.clinerules</span> in deinem Arbeitsbereich geladen werden."
44+
"loadFromFile": "Anweisungen können auch aus <span>.roorules</span> in deinem Arbeitsbereich geladen werden (.clinerules ist veraltet und wird bald nicht mehr funktionieren)."
4545
},
4646
"systemPrompt": {
4747
"preview": "System-Prompt Vorschau",

webview-ui/src/i18n/locales/en/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Mode-specific Custom Instructions (optional)",
3737
"resetToDefault": "Reset to default",
3838
"description": "Add behavioral guidelines specific to {{modeName}} mode.",
39-
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from <span>.clinerules-{{slug}}</span> in your workspace."
39+
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from <span>.roorules-{{slug}}</span> in your workspace (.clinerules-{{slug}} is deprecated and will stop working soon)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Custom Instructions for All Modes",
4343
"description": "These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below.\nIf you would like Roo to think and speak in a different language than your editor display language ({{language}}), you can specify it here.",
44-
"loadFromFile": "Instructions can also be loaded from <span>.clinerules</span> in your workspace."
44+
"loadFromFile": "Instructions can also be loaded from <span>.roorules</span> in your workspace (.clinerules is deprecated and will stop working soon)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Preview System Prompt",

webview-ui/src/i18n/locales/es/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instrucciones personalizadas para el modo (opcional)",
3737
"resetToDefault": "Restablecer a valores predeterminados",
3838
"description": "Agrega directrices de comportamiento específicas para el modo {{modeName}}.",
39-
"loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde <span>.clinerules-{{slug}}</span> en tu espacio de trabajo."
39+
"loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde <span>.roorules-{{slug}}</span> en tu espacio de trabajo (.clinerules-{{slug}} está obsoleto y dejará de funcionar pronto)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instrucciones personalizadas para todos los modos",
4343
"description": "Estas instrucciones se aplican a todos los modos. Proporcionan un conjunto base de comportamientos que pueden ser mejorados por instrucciones específicas de cada modo.\nSi quieres que Roo piense y hable en un idioma diferente al idioma de visualización de tu editor ({{language}}), puedes especificarlo aquí.",
44-
"loadFromFile": "Las instrucciones también se pueden cargar desde <span>.clinerules</span> en tu espacio de trabajo."
44+
"loadFromFile": "Las instrucciones también se pueden cargar desde <span>.roorules</span> en tu espacio de trabajo (.clinerules está obsoleto y dejará de funcionar pronto)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Vista previa de la solicitud del sistema",

webview-ui/src/i18n/locales/fr/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instructions personnalisées spécifiques au mode (optionnel)",
3737
"resetToDefault": "Réinitialiser aux valeurs par défaut",
3838
"description": "Ajoutez des directives comportementales spécifiques au mode {{modeName}}.",
39-
"loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis <span>.clinerules-{{slug}}</span> dans votre espace de travail."
39+
"loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis <span>.roorules-{{slug}}</span> dans votre espace de travail (.clinerules-{{slug}} est obsolète et cessera de fonctionner bientôt)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instructions personnalisées pour tous les modes",
4343
"description": "Ces instructions s'appliquent à tous les modes. Elles fournissent un ensemble de comportements de base qui peuvent être améliorés par des instructions spécifiques au mode ci-dessous.\nSi vous souhaitez que Roo pense et parle dans une langue différente de celle de votre éditeur ({{language}}), vous pouvez le spécifier ici.",
44-
"loadFromFile": "Les instructions peuvent également être chargées depuis <span>.clinerules</span> dans votre espace de travail."
44+
"loadFromFile": "Les instructions peuvent également être chargées depuis <span>.roorules</span> dans votre espace de travail (.clinerules est obsolète et cessera de fonctionner bientôt)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Aperçu du prompt système",

0 commit comments

Comments
 (0)