Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
590 changes: 295 additions & 295 deletions src/core/prompts/__tests__/__snapshots__/system.test.ts.snap

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,9 @@ type GlobalSettings = {
roleDefinition: string
customInstructions?: string | undefined
groups: (
| ("read" | "edit" | "browser" | "command" | "mcp" | "modes")
| ("read" | "edit" | "browser" | "command" | "mcp" | "subtask" | "switch" | "followup")
| [
"read" | "edit" | "browser" | "command" | "mcp" | "modes",
"read" | "edit" | "browser" | "command" | "mcp" | "subtask" | "switch" | "followup",
{
fileRegex?: string | undefined
description?: string | undefined
Expand Down
4 changes: 2 additions & 2 deletions src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,9 @@ type GlobalSettings = {
roleDefinition: string
customInstructions?: string | undefined
groups: (
| ("read" | "edit" | "browser" | "command" | "mcp" | "modes")
| ("read" | "edit" | "browser" | "command" | "mcp" | "subtask" | "switch" | "followup")
| [
"read" | "edit" | "browser" | "command" | "mcp" | "modes",
"read" | "edit" | "browser" | "command" | "mcp" | "subtask" | "switch" | "followup",
{
fileRegex?: string | undefined
description?: string | undefined
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type ProviderName = z.infer<typeof providerNamesSchema>
* ToolGroup
*/

export const toolGroups = ["read", "edit", "browser", "command", "mcp", "modes"] as const
export const toolGroups = ["read", "edit", "browser", "command", "mcp", "subtask", "switch", "followup"] as const

export const toolGroupsSchema = z.enum(toolGroups)

Expand Down
110 changes: 107 additions & 3 deletions src/shared/__tests__/modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ describe("isToolAllowedForMode", () => {
},
]

it("allows always available tools", () => {
expect(isToolAllowedForMode("ask_followup_question", "markdown-editor", customModes)).toBe(true)
it("checks tool availability: always allowed and permission-based for 'markdown-editor'", () => {
// 'attempt_completion' is always allowed for any mode.
expect(isToolAllowedForMode("attempt_completion", "markdown-editor", customModes)).toBe(true)

// The 'markdown-editor' custom mode (defined in customModes array) lacks 'followup', 'subtask', and 'switch' permissions.
// These tests verify that tools requiring these specific permissions are correctly disallowed for 'markdown-editor'.
expect(isToolAllowedForMode("ask_followup_question", "markdown-editor", customModes)).toBe(false) // Requires 'followup'
expect(isToolAllowedForMode("new_task", "markdown-editor", customModes)).toBe(false) // Requires 'subtask'
expect(isToolAllowedForMode("switch_mode", "markdown-editor", customModes)).toBe(false) // Requires 'switch'
})

it("allows unrestricted tools", () => {
Expand Down Expand Up @@ -256,6 +262,104 @@ describe("isToolAllowedForMode", () => {
})
})

// New tests for default built-in mode permissions
describe("default permissions for built-in modes", () => {
const builtInModes: ModeConfig[] = [] // Use empty array as customModes to test against built-in definitions
const modesToTest = ["architect", "ask", "debug", "orchestrator"]
const toolsToCheck = ["new_task", "switch_mode", "ask_followup_question"]

modesToTest.forEach((modeSlug) => {
describe(`mode: ${modeSlug}`, () => {
toolsToCheck.forEach((toolName) => {
/**
* @test {isToolAllowedForMode} Verifies default tool permissions for built-in modes.
* These tests are expected to FAIL until the corresponding groups ('subtask', 'switch', 'followup')
* are added to the built-in mode definitions in src/shared/modes.ts.
*/
it(`should allow tool: ${toolName}`, () => {
// We expect this to be true eventually, but it should fail initially
expect(isToolAllowedForMode(toolName as any, modeSlug, builtInModes)).toBe(true)
})
})
})
})
})
// End of new tests
describe('when tool group is "subtask"', () => {
const toolName = "new_task"
const requiredGroup = "subtask"

it('should return true if mode has the "subtask" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode", // roleDefinition is required by ModeConfig
groups: [requiredGroup], // Use groups array
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(true)
})

it('should return false if mode does not have the "subtask" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode",
groups: [], // Do not include the required group
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(false)
})
})

describe('when tool group is "switch"', () => {
const toolName = "switch_mode"
const requiredGroup = "switch"

it('should return true if mode has the "switch" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode",
groups: [requiredGroup],
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(true)
})

it('should return false if mode does not have the "switch" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode",
groups: [],
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(false)
})
})

describe('when tool group is "followup"', () => {
const toolName = "ask_followup_question"
const requiredGroup = "followup"

it('should return true if mode has the "followup" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode",
groups: [requiredGroup],
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(true)
})

it('should return false if mode does not have the "followup" group', () => {
const mode: ModeConfig = {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "A test mode",
groups: [],
}
expect(isToolAllowedForMode(toolName, mode.slug, [mode])).toBe(false)
})
})

describe("FileRestrictionError", () => {
it("formats error message with pattern when no description provided", () => {
const error = new FileRestrictionError("Markdown Editor", "\\.md$", undefined, "test.js")
Expand All @@ -274,7 +378,7 @@ describe("FileRestrictionError", () => {
name: "🪲 Debug",
roleDefinition:
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
groups: ["read", "edit", "browser", "command", "mcp"],
groups: ["read", "edit", "browser", "command", "mcp", "subtask", "switch", "followup"],
})
expect(debugMode?.customInstructions).toContain(
"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",
Expand Down
20 changes: 14 additions & 6 deletions src/shared/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,22 @@ export const modes: readonly ModeConfig[] = [
name: "💻 Code",
roleDefinition:
"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
groups: ["read", "edit", "browser", "command", "mcp"],
groups: ["read", "edit", "browser", "command", "mcp", "followup", "switch", "subtask"],
},
{
slug: "architect",
name: "🏗️ Architect",
roleDefinition:
"You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
groups: [
"read",
["edit", { fileRegex: "\\.md$", description: "Markdown files only" }],
"browser",
"mcp",
"subtask",
"switch",
"followup",
],
customInstructions:
"1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.",
},
Expand All @@ -73,7 +81,7 @@ export const modes: readonly ModeConfig[] = [
name: "❓ Ask",
roleDefinition:
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.",
groups: ["read", "browser", "mcp"],
groups: ["read", "browser", "mcp", "subtask", "switch", "followup"],
customInstructions:
"You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.",
},
Expand All @@ -82,7 +90,7 @@ export const modes: readonly ModeConfig[] = [
name: "🪲 Debug",
roleDefinition:
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
groups: ["read", "edit", "browser", "command", "mcp"],
groups: ["read", "edit", "browser", "command", "mcp", "subtask", "switch", "followup"],
customInstructions:
"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",
},
Expand All @@ -91,7 +99,7 @@ export const modes: readonly ModeConfig[] = [
name: "🪃 Orchestrator",
roleDefinition:
"You are Roo, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.",
groups: [],
groups: ["subtask", "switch", "followup"],
customInstructions:
"Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you're delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.",
},
Expand Down Expand Up @@ -209,7 +217,7 @@ export function isToolAllowedForMode(
}

// For the edit group, check file regex if specified
if (groupName === "edit" && options.fileRegex) {
if (groupName === "edit" && options && options.fileRegex) {
const filePath = toolParams?.path
if (
filePath &&
Expand Down
18 changes: 9 additions & 9 deletions src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,19 +202,19 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
mcp: {
tools: ["use_mcp_tool", "access_mcp_resource"],
},
modes: {
tools: ["switch_mode", "new_task"],
alwaysAvailable: true,
subtask: {
tools: ["new_task"],
},
switch: {
tools: ["switch_mode"],
},
followup: {
tools: ["ask_followup_question"],
},
}

// Tools that are always available to all modes.
export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"new_task",
] as const
export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = ["attempt_completion"] as const

export type DiffResult =
| { success: true; content: string; failParts?: DiffResult[] }
Expand Down
101 changes: 64 additions & 37 deletions webview-ui/src/components/prompts/PromptsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -653,41 +653,50 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
{isToolsEditMode && findModeBySlug(visualMode, customModes) ? (
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
{availableGroups.map((group) => {
{(Object.keys(TOOL_GROUPS) as ToolGroup[]).map((group) => {
const currentMode = getCurrentMode()
const isCustomMode = findModeBySlug(visualMode, customModes)
const customMode = isCustomMode
const isGroupEnabled = isCustomMode
? customMode?.groups?.some((g) => getGroupName(g) === group)
: currentMode?.groups?.some((g) => getGroupName(g) === group)

// Check if the group should be always available and thus potentially non-editable
// For now, we allow editing all for custom modes as per test requirements
const isDisabled = !isCustomMode

// Get description information
const groupEntry = currentMode?.groups?.find((g) => getGroupName(g) === group)
const options = Array.isArray(groupEntry) ? groupEntry[1] : undefined
let description = ""

if (group === "edit" && options?.fileRegex) {
description = options.description || `/${options.fileRegex}/`
description = `${t("prompts:tools.allowedFiles")}: ${description}`
}
// Removed slugRegex logic for subtask/switch groups

return (
<VSCodeCheckbox
key={group}
checked={isGroupEnabled}
onChange={handleGroupChange(group, Boolean(isCustomMode), customMode)}
disabled={!isCustomMode}>
{t(`prompts:tools.toolNames.${group}`)}
{group === "edit" && (
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
{t("prompts:tools.allowedFiles")}{" "}
{(() => {
const currentMode = getCurrentMode()
const editGroup = currentMode?.groups?.find(
(g) =>
Array.isArray(g) &&
g[0] === "edit" &&
g[1]?.fileRegex,
)
if (!Array.isArray(editGroup)) return t("prompts:allFiles")
return (
editGroup[1].description ||
`/${editGroup[1].fileRegex}/`
)
})()}
<div key={group} className="mb-2">
<VSCodeCheckbox
checked={isGroupEnabled}
data-testid={`tool-permission-${group}-checkbox`}
onChange={handleGroupChange(
group,
Boolean(isCustomMode),
customMode,
)}
disabled={isDisabled}>
{t(`prompts:tools.toolNames.${group}`)}
</VSCodeCheckbox>
{description && (
<div
className="text-xs text-vscode-descriptionForeground mt-0.5 ml-6 w-full break-words"
data-testid={`tool-permission-${group}-description`}>
{description}
</div>
)}
</VSCodeCheckbox>
</div>
)
})}
</div>
Expand All @@ -696,18 +705,36 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{(() => {
const currentMode = getCurrentMode()
const enabledGroups = currentMode?.groups || []
return enabledGroups
.map((group) => {
const groupName = getGroupName(group)
const displayName = t(`prompts:tools.toolNames.${groupName}`)
if (Array.isArray(group) && group[1]?.fileRegex) {
const description =
group[1].description || `/${group[1].fileRegex}/`
return `${displayName} (${description})`
}
return displayName
})
.join(", ")
if (enabledGroups.length === 0) {
return (
<div className="text-vscode-descriptionForeground">
{t("prompts:tools.noToolsEnabled")}
</div>
)
}
return enabledGroups.map((group) => {
const groupName = getGroupName(group)
const options = Array.isArray(group) ? group[1] : undefined
const displayName = t(`prompts:tools.toolNames.${groupName}`)
let description = ""

if (groupName === "edit" && options?.fileRegex) {
description = options.description || `/${options.fileRegex}/`
}
// Removed slugRegex logic for subtask/switch groups

return (
<div key={groupName} data-testid={`tool-group-label-${groupName}`}>
<span>{displayName}</span>
{/* Render description directly if it exists (already includes parentheses) */}
{description && (
<span className="ml-1 text-xs text-vscode-descriptionForeground">
{description}
</span>
)}
</div>
)
})
})()}
</div>
)}
Expand Down
Loading