Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
440 changes: 406 additions & 34 deletions src/core/prompts/sections/__tests__/custom-instructions.test.ts

Large diffs are not rendered by default.

113 changes: 104 additions & 9 deletions src/core/prompts/sections/custom-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import path from "path"

import { LANGUAGES, isLanguage } from "../../../shared/language"

/**
* Safely read a file and return its trimmed content
*/
async function safeReadFile(filePath: string): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is identical to the existing implementation. Consider importing and reusing the existing function instead.

try {
const content = await fs.readFile(filePath, "utf-8")
Expand All @@ -16,7 +19,81 @@ async function safeReadFile(filePath: string): Promise<string> {
}
}

/**
* Check if a directory exists
*/
async function directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath)
return stats.isDirectory()
} catch (err) {
return false
}
}

/**
* Read all text files from a directory in alphabetical order
*/
async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ filename: string; content: string }>> {
try {
const files = await fs
.readdir(dirPath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((files) => files.map((file) => path.resolve(dirPath, file.name)))

const fileContents = await Promise.all(
files.map(async (file) => {
const filePath = path.join(dirPath, file)
try {
// Check if it's a file (not a directory)
const stats = await fs.stat(filePath)
if (stats.isFile()) {
const content = await safeReadFile(filePath)
return { filename: file, content }
}
return null
} catch (err) {
return null
}
}),
)

// Filter out null values (directories or failed reads)
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
} catch (err) {
return []
}
}

/**
* Format content from multiple files with filenames as headers
*/
function formatDirectoryContent(dirPath: string, files: Array<{ filename: string; content: string }>): string {
if (files.length === 0) return ""

const formattedContent = files
.map((file) => {
return `# Filename: ${file.filename}\n${file.content}`
})
.join("\n\n")

return `\n# Rules from ${path.relative(process.cwd(), dirPath)}:\n${formattedContent}\n`
}

/**
* Load rule files from the specified directory
*/
export async function loadRuleFiles(cwd: string): Promise<string> {
// Check for .roo/rules/ directory
const rooRulesDir = path.join(cwd, ".roo", "rules")
if (await directoryExists(rooRulesDir)) {
const files = await readTextFilesFromDirectory(rooRulesDir)
if (files.length > 0) {
return formatDirectoryContent(rooRulesDir, files)
}
}

// Fall back to existing behavior
const ruleFiles = [".roorules", ".clinerules"]

for (const file of ruleFiles) {
Expand All @@ -41,16 +118,30 @@ export async function addCustomInstructions(
// Load mode-specific rules if mode is provided
let modeRuleContent = ""
let usedRuleFile = ""

if (mode) {
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))
// Check for .roo/rules-${mode}/ directory
const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`)
if (await directoryExists(modeRulesDir)) {
const files = await readTextFilesFromDirectory(modeRulesDir)
if (files.length > 0) {
modeRuleContent = formatDirectoryContent(modeRulesDir, files)
usedRuleFile = modeRulesDir
}
}

// If no directory exists, fall back to existing behavior
if (!modeRuleContent) {
const rooModeRuleFile = `.roorules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
if (modeRuleContent) {
usedRuleFile = clineModeRuleFile
usedRuleFile = rooModeRuleFile
} else {
const clineModeRuleFile = `.clinerules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
if (modeRuleContent) {
usedRuleFile = clineModeRuleFile
}
}
}
}
Expand Down Expand Up @@ -78,7 +169,11 @@ export async function addCustomInstructions(

// Add mode-specific rules first if they exist
if (modeRuleContent && modeRuleContent.trim()) {
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
if (usedRuleFile.includes(path.join(".roo", `rules-${mode}`))) {
rules.push(modeRuleContent.trim())
} else {
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
}
}

if (options.rooIgnoreInstructions) {
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/prompts/PromptsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
// Open or create an empty file
vscode.postMessage({
type: "openFile",
text: `./.roorules-${currentMode.slug}`,
text: `./.roo/rules-${currentMode.slug}/rules.md`,
values: {
create: true,
content: "",
Expand Down Expand Up @@ -935,7 +935,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.roorules",
text: "./.roo/rules/rules.md",
values: {
create: true,
content: "",
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/i18n/locales/ca/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"title": "Instruccions personalitzades específiques del mode (opcional)",
"resetToDefault": "Restablir a valors predeterminats",
"description": "Afegiu directrius de comportament específiques per al mode {{modeName}}.",
"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)."
"loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de la carpeta <span>.roo/rules-{{slug}}/</span> al vostre espai de treball (.roorules-{{slug}} i .clinerules-{{slug}} estan obsolets i deixaran de funcionar aviat)."
},
"globalCustomInstructions": {
"title": "Instruccions personalitzades per a tots els modes",
"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í.",
"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)."
"loadFromFile": "Les instruccions també es poden carregar des de la carpeta <span>.roo/rules/</span> al vostre espai de treball (.roorules i .clinerules estan obsolets i deixaran de funcionar aviat)."
},
"systemPrompt": {
"preview": "Previsualització del prompt del sistema",
Expand Down Expand Up @@ -100,7 +100,7 @@
},
"advancedSystemPrompt": {
"title": "Avançat: Sobreescriure prompt del sistema",
"description": "Podeu reemplaçar completament el prompt del sistema per a aquest mode (a part de la definició de rol i instruccions personalitzades) creant un fitxer a <span>.roo/system-prompt-{{slug}}</span> al vostre espai de treball. Aquesta és una funcionalitat molt avançada que eludeix les salvaguardes integrades i les comprovacions de consistència (especialment al voltant de l'ús d'eines), així que aneu amb compte!"
"description": "Podeu reemplaçar completament el prompt del sistema per a aquest mode (a part de la definició de rol i instruccions personalitzades) creant un fitxer a la carpeta <span>.roo/system-prompts/{{slug}}/</span> al vostre espai de treball. Aquesta és una funcionalitat molt avançada que eludeix les salvaguardes integrades i les comprovacions de consistència (especialment al voltant de l'ús d'eines), així que aneu amb compte!"
},
"createModeDialog": {
"title": "Crear nou mode",
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/i18n/locales/de/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"title": "Modusspezifische benutzerdefinierte Anweisungen (optional)",
"resetToDefault": "Auf Standardwerte zurücksetzen",
"description": "Fügen Sie verhaltensspezifische Richtlinien für den Modus {{modeName}} hinzu.",
"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)."
"loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus dem Ordner <span>.roo/rules-{{slug}}/</span> in deinem Arbeitsbereich geladen werden (.roorules-{{slug}} und .clinerules-{{slug}} sind veraltet und werden bald nicht mehr funktionieren)."
},
"globalCustomInstructions": {
"title": "Benutzerdefinierte Anweisungen für alle Modi",
"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.",
"loadFromFile": "Anweisungen können auch aus <span>.roorules</span> in deinem Arbeitsbereich geladen werden (.clinerules ist veraltet und wird bald nicht mehr funktionieren)."
"loadFromFile": "Anweisungen können auch aus dem Ordner <span>.roo/rules/</span> in deinem Arbeitsbereich geladen werden (.roorules und .clinerules sind veraltet und werden bald nicht mehr funktionieren)."
},
"systemPrompt": {
"preview": "System-Prompt Vorschau",
Expand Down Expand Up @@ -100,7 +100,7 @@
},
"advancedSystemPrompt": {
"title": "Erweitert: System-Prompt überschreiben",
"description": "Du kannst den System-Prompt für diesen Modus vollständig ersetzen (abgesehen von der Rollendefinition und benutzerdefinierten Anweisungen), indem du eine Datei unter <span>.roo/system-prompt-{{slug}}</span> in deinem Arbeitsbereich erstellst. Dies ist eine sehr fortgeschrittene Funktion, die eingebaute Schutzmaßnahmen und Konsistenzprüfungen umgeht (besonders bei der Werkzeugnutzung), also sei vorsichtig!"
"description": "Du kannst den System-Prompt für diesen Modus vollständig ersetzen (abgesehen von der Rollendefinition und benutzerdefinierten Anweisungen), indem du eine Datei im Ordner <span>.roo/system-prompts/{{slug}}/</span> in deinem Arbeitsbereich erstellst. Dies ist eine sehr fortgeschrittene Funktion, die eingebaute Schutzmaßnahmen und Konsistenzprüfungen umgeht (besonders bei der Werkzeugnutzung), also sei vorsichtig!"
},
"createModeDialog": {
"title": "Neuen Modus erstellen",
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/i18n/locales/en/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"title": "Mode-specific Custom Instructions (optional)",
"resetToDefault": "Reset to default",
"description": "Add behavioral guidelines specific to {{modeName}} mode.",
"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)."
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from the <span>.roo/rules-{{slug}}/</span> folder in your workspace (.roorules-{{slug}} and .clinerules-{{slug}} are deprecated and will stop working soon)."
},
"globalCustomInstructions": {
"title": "Custom Instructions for All Modes",
"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.",
"loadFromFile": "Instructions can also be loaded from <span>.roorules</span> in your workspace (.clinerules is deprecated and will stop working soon)."
"loadFromFile": "Instructions can also be loaded from the <span>.roo/rules/</span> folder in your workspace (.roorules and .clinerules are deprecated and will stop working soon)."
},
"systemPrompt": {
"preview": "Preview System Prompt",
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/i18n/locales/es/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"title": "Instrucciones personalizadas para el modo (opcional)",
"resetToDefault": "Restablecer a valores predeterminados",
"description": "Agrega directrices de comportamiento específicas para el modo {{modeName}}.",
"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)."
"loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde la carpeta <span>.roo/rules-{{slug}}/</span> en tu espacio de trabajo (.roorules-{{slug}} y .clinerules-{{slug}} están obsoletos y dejarán de funcionar pronto)."
},
"globalCustomInstructions": {
"title": "Instrucciones personalizadas para todos los modos",
"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í.",
"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)."
"loadFromFile": "Las instrucciones también se pueden cargar desde la carpeta <span>.roo/rules/</span> en tu espacio de trabajo (.roorules y .clinerules están obsoletos y dejarán de funcionar pronto)."
},
"systemPrompt": {
"preview": "Vista previa de la solicitud del sistema",
Expand Down Expand Up @@ -100,7 +100,7 @@
},
"advancedSystemPrompt": {
"title": "Avanzado: Anular solicitud del sistema",
"description": "Puedes reemplazar completamente la solicitud del sistema para este modo (aparte de la definición de rol e instrucciones personalizadas) creando un archivo en <span>.roo/system-prompt-{{slug}}</span> en tu espacio de trabajo. ¡Esta es una función muy avanzada que omite las salvaguardas integradas y las verificaciones de consistencia (especialmente en torno al uso de herramientas), así que ten cuidado!"
"description": "Puedes reemplazar completamente la solicitud del sistema para este modo (aparte de la definición de rol e instrucciones personalizadas) creando un archivo en la carpeta <span>.roo/system-prompts/{{slug}}/</span> en tu espacio de trabajo. ¡Esta es una función muy avanzada que omite las salvaguardas integradas y las verificaciones de consistencia (especialmente en torno al uso de herramientas), así que ten cuidado!"
},
"createModeDialog": {
"title": "Crear nuevo modo",
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/i18n/locales/fr/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"title": "Instructions personnalisées spécifiques au mode (optionnel)",
"resetToDefault": "Réinitialiser aux valeurs par défaut",
"description": "Ajoutez des directives comportementales spécifiques au mode {{modeName}}.",
"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)."
"loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis le dossier <span>.roo/rules-{{slug}}/</span> dans votre espace de travail (.roorules-{{slug}} et .clinerules-{{slug}} sont obsolètes et cesseront de fonctionner bientôt)."
},
"globalCustomInstructions": {
"title": "Instructions personnalisées pour tous les modes",
"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.",
"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)."
"loadFromFile": "Les instructions peuvent également être chargées depuis le dossier <span>.roo/rules/</span> dans votre espace de travail (.roorules et .clinerules sont obsolètes et cesseront de fonctionner bientôt)."
},
"systemPrompt": {
"preview": "Aperçu du prompt système",
Expand Down Expand Up @@ -100,7 +100,7 @@
},
"advancedSystemPrompt": {
"title": "Avancé : Remplacer le prompt système",
"description": "Vous pouvez complètement remplacer le prompt système pour ce mode (en dehors de la définition du rôle et des instructions personnalisées) en créant un fichier à <span>.roo/system-prompt-{{slug}}</span> dans votre espace de travail. Il s'agit d'une fonctionnalité très avancée qui contourne les garanties intégrées et les vérifications de cohérence (notamment concernant l'utilisation des outils), alors soyez prudent !"
"description": "Vous pouvez complètement remplacer le prompt système pour ce mode (en dehors de la définition du rôle et des instructions personnalisées) en créant un fichier dans le dossier <span>.roo/system-prompts/{{slug}}/</span> dans votre espace de travail. Il s'agit d'une fonctionnalité très avancée qui contourne les garanties intégrées et les vérifications de cohérence (notamment concernant l'utilisation des outils), alors soyez prudent !"
},
"createModeDialog": {
"title": "Créer un nouveau mode",
Expand Down
Loading