mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-22 13:21:07 -05:00
Merge pull request #523 from RooVetGit/allow_architect_mode_to_write_markdown
Allow architect and ask modes to write md files
This commit is contained in:
188
src/shared/__tests__/modes.test.ts
Normal file
188
src/shared/__tests__/modes.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
|
||||
|
||||
describe("isToolAllowedForMode", () => {
|
||||
const customModes: ModeConfig[] = [
|
||||
{
|
||||
slug: "markdown-editor",
|
||||
name: "Markdown Editor",
|
||||
roleDefinition: "You are a markdown editor",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
|
||||
},
|
||||
{
|
||||
slug: "css-editor",
|
||||
name: "CSS Editor",
|
||||
roleDefinition: "You are a CSS editor",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
|
||||
},
|
||||
]
|
||||
|
||||
it("allows always available tools", () => {
|
||||
expect(isToolAllowedForMode("ask_followup_question", "markdown-editor", customModes)).toBe(true)
|
||||
expect(isToolAllowedForMode("attempt_completion", "markdown-editor", customModes)).toBe(true)
|
||||
})
|
||||
|
||||
it("allows unrestricted tools", () => {
|
||||
expect(isToolAllowedForMode("read_file", "markdown-editor", customModes)).toBe(true)
|
||||
expect(isToolAllowedForMode("browser_action", "markdown-editor", customModes)).toBe(true)
|
||||
})
|
||||
|
||||
describe("file restrictions", () => {
|
||||
it("allows editing matching files", () => {
|
||||
// Test markdown editor mode
|
||||
const mdResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
|
||||
path: "test.md",
|
||||
})
|
||||
expect(mdResult).toBe(true)
|
||||
|
||||
// Test CSS editor mode
|
||||
const cssResult = isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
|
||||
path: "styles.css",
|
||||
})
|
||||
expect(cssResult).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects editing non-matching files", () => {
|
||||
// Test markdown editor mode with non-markdown file
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(FileRestrictionError)
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(/\\.md\$/)
|
||||
|
||||
// Test CSS editor mode with non-CSS file
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(FileRestrictionError)
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(/\\.css\$/)
|
||||
})
|
||||
|
||||
it("applies restrictions to both write_to_file and apply_diff", () => {
|
||||
// Test write_to_file
|
||||
const writeResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
|
||||
path: "test.md",
|
||||
})
|
||||
expect(writeResult).toBe(true)
|
||||
|
||||
// Test apply_diff
|
||||
const diffResult = isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
|
||||
path: "test.md",
|
||||
})
|
||||
expect(diffResult).toBe(true)
|
||||
|
||||
// Test both with non-matching file
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(FileRestrictionError)
|
||||
|
||||
expect(() =>
|
||||
isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, { path: "test.js" }),
|
||||
).toThrow(FileRestrictionError)
|
||||
})
|
||||
|
||||
it("uses description in file restriction error for custom modes", () => {
|
||||
const customModesWithDescription: ModeConfig[] = [
|
||||
{
|
||||
slug: "docs-editor",
|
||||
name: "Documentation Editor",
|
||||
roleDefinition: "You are a documentation editor",
|
||||
groups: [
|
||||
"read",
|
||||
["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
|
||||
"browser",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Test write_to_file with non-matching file
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.js",
|
||||
}),
|
||||
).toThrow(FileRestrictionError)
|
||||
expect(() =>
|
||||
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.js",
|
||||
}),
|
||||
).toThrow(/Documentation files only/)
|
||||
|
||||
// Test apply_diff with non-matching file
|
||||
expect(() =>
|
||||
isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.js",
|
||||
}),
|
||||
).toThrow(FileRestrictionError)
|
||||
expect(() =>
|
||||
isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.js",
|
||||
}),
|
||||
).toThrow(/Documentation files only/)
|
||||
|
||||
// Test that matching files are allowed
|
||||
expect(
|
||||
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.md",
|
||||
}),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
|
||||
path: "test.txt",
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("allows ask mode to edit markdown files only", () => {
|
||||
// Should allow editing markdown files
|
||||
expect(isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.md" })).toBe(true)
|
||||
|
||||
// Should allow applying diffs to markdown files
|
||||
expect(isToolAllowedForMode("apply_diff", "ask", [], undefined, { path: "readme.md" })).toBe(true)
|
||||
|
||||
// Should reject non-markdown files
|
||||
expect(() => isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.js" })).toThrow(
|
||||
FileRestrictionError,
|
||||
)
|
||||
expect(() => isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.js" })).toThrow(
|
||||
/Markdown files only/,
|
||||
)
|
||||
|
||||
// Should maintain read capabilities
|
||||
expect(isToolAllowedForMode("read_file", "ask", [])).toBe(true)
|
||||
expect(isToolAllowedForMode("browser_action", "ask", [])).toBe(true)
|
||||
expect(isToolAllowedForMode("use_mcp_tool", "ask", [])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("handles non-existent modes", () => {
|
||||
expect(isToolAllowedForMode("write_to_file", "non-existent", customModes)).toBe(false)
|
||||
})
|
||||
|
||||
it("respects tool requirements", () => {
|
||||
const toolRequirements = {
|
||||
write_to_file: false,
|
||||
}
|
||||
|
||||
expect(isToolAllowedForMode("write_to_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileRestrictionError", () => {
|
||||
it("formats error message with pattern when no description provided", () => {
|
||||
const error = new FileRestrictionError("Markdown Editor", "\\.md$", undefined, "test.js")
|
||||
expect(error.message).toBe(
|
||||
"This mode (Markdown Editor) can only edit files matching pattern: \\.md$. Got: test.js",
|
||||
)
|
||||
expect(error.name).toBe("FileRestrictionError")
|
||||
})
|
||||
|
||||
it("formats error message with description when provided", () => {
|
||||
const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js")
|
||||
expect(error.message).toBe(
|
||||
"This mode (Markdown Editor) can only edit files matching pattern: \\.md$ (Markdown files only). Got: test.js",
|
||||
)
|
||||
expect(error.name).toBe("FileRestrictionError")
|
||||
})
|
||||
})
|
||||
@@ -3,13 +3,22 @@ import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
|
||||
// Mode types
|
||||
export type Mode = string
|
||||
|
||||
// Group options type
|
||||
export type GroupOptions = {
|
||||
fileRegex?: string // Regular expression pattern
|
||||
description?: string // Human-readable description of the pattern
|
||||
}
|
||||
|
||||
// Group entry can be either a string or tuple with options
|
||||
export type GroupEntry = ToolGroup | readonly [ToolGroup, GroupOptions]
|
||||
|
||||
// Mode configuration type
|
||||
export type ModeConfig = {
|
||||
slug: string
|
||||
name: string
|
||||
roleDefinition: string
|
||||
customInstructions?: string
|
||||
groups: readonly ToolGroup[] // Now uses groups instead of tools array
|
||||
groups: readonly GroupEntry[] // Now supports both simple strings and tuples with options
|
||||
}
|
||||
|
||||
// Mode-specific prompts only
|
||||
@@ -22,13 +31,35 @@ export type CustomModePrompts = {
|
||||
[key: string]: PromptComponent | undefined
|
||||
}
|
||||
|
||||
// Helper to extract group name regardless of format
|
||||
export function getGroupName(group: GroupEntry): ToolGroup {
|
||||
return Array.isArray(group) ? group[0] : group
|
||||
}
|
||||
|
||||
// Helper to get group options if they exist
|
||||
function getGroupOptions(group: GroupEntry): GroupOptions | undefined {
|
||||
return Array.isArray(group) ? group[1] : undefined
|
||||
}
|
||||
|
||||
// Helper to check if a file path matches a regex pattern
|
||||
export function doesFileMatchRegex(filePath: string, pattern: string): boolean {
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
return regex.test(filePath)
|
||||
} catch (error) {
|
||||
console.error(`Invalid regex pattern: ${pattern}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get all tools for a mode
|
||||
export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
|
||||
export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
||||
const tools = new Set<string>()
|
||||
|
||||
// Add tools from each group
|
||||
groups.forEach((group) => {
|
||||
TOOL_GROUPS[group].forEach((tool) => tools.add(tool))
|
||||
const groupName = getGroupName(group)
|
||||
TOOL_GROUPS[groupName].forEach((tool) => tools.add(tool))
|
||||
})
|
||||
|
||||
// Always add required tools
|
||||
@@ -50,15 +81,15 @@ export const modes: readonly ModeConfig[] = [
|
||||
slug: "architect",
|
||||
name: "Architect",
|
||||
roleDefinition:
|
||||
"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
|
||||
groups: ["read", "browser", "mcp"],
|
||||
"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements. You can edit markdown documentation files to help document architectural decisions and patterns.",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
|
||||
},
|
||||
{
|
||||
slug: "ask",
|
||||
name: "Ask",
|
||||
roleDefinition:
|
||||
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||
groups: ["read", "browser", "mcp"],
|
||||
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -113,11 +144,22 @@ export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean
|
||||
return !!customModes?.some((mode) => mode.slug === slug)
|
||||
}
|
||||
|
||||
// Custom error class for file restrictions
|
||||
export class FileRestrictionError extends Error {
|
||||
constructor(mode: string, pattern: string, description: string | undefined, filePath: string) {
|
||||
super(
|
||||
`This mode (${mode}) can only edit files matching pattern: ${pattern}${description ? ` (${description})` : ""}. Got: ${filePath}`,
|
||||
)
|
||||
this.name = "FileRestrictionError"
|
||||
}
|
||||
}
|
||||
|
||||
export function isToolAllowedForMode(
|
||||
tool: string,
|
||||
modeSlug: string,
|
||||
customModes: ModeConfig[],
|
||||
toolRequirements?: Record<string, boolean>,
|
||||
toolParams?: Record<string, any>, // All tool parameters
|
||||
): boolean {
|
||||
// Always allow these tools
|
||||
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
|
||||
@@ -136,8 +178,35 @@ export function isToolAllowedForMode(
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if tool is in any of the mode's groups
|
||||
return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string))
|
||||
// Check if tool is in any of the mode's groups and respects any group options
|
||||
for (const group of mode.groups) {
|
||||
const groupName = getGroupName(group)
|
||||
const options = getGroupOptions(group)
|
||||
|
||||
// If the tool isn't in this group, continue to next group
|
||||
if (!TOOL_GROUPS[groupName].includes(tool)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If there are no options, allow the tool
|
||||
if (!options) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For the edit group, check file regex if specified
|
||||
if (groupName === "edit" && options.fileRegex) {
|
||||
const filePath = toolParams?.path
|
||||
// Only validate regex if a path is provided
|
||||
if (filePath && !doesFileMatchRegex(filePath, options.fileRegex)) {
|
||||
throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Create the mode-specific default prompts
|
||||
|
||||
Reference in New Issue
Block a user