Skip to content
Open
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
91 changes: 91 additions & 0 deletions examples/hidden-submodes.roomodes
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Example .roomodes file demonstrating hidden submodes feature
# Hidden modes are not shown in the mode selector dropdown but can be accessed
# programmatically by their parent mode using the switch_mode tool

customModes:
# Parent mode that can delegate to specialized submodes
- slug: complex-task-handler
name: 🎯 Complex Task Handler
roleDefinition: |-
You are a complex task handler that breaks down large tasks into specialized subtasks.
You can delegate specific work to hidden specialized submodes.
whenToUse: Use this mode for complex tasks that require multiple specialized approaches
description: Handles complex multi-step tasks
groups:
- read
- edit
- command
customInstructions: |-
When handling complex tasks:
1. Analyze the task requirements
2. Identify specialized subtasks
3. Use switch_mode to delegate to appropriate hidden submodes:
- data-analyzer: For data analysis tasks
- code-generator: For code generation tasks
- test-writer: For test writing tasks
4. Coordinate results from submodes
5. Provide comprehensive solution

# Hidden submode for data analysis (only accessible from complex-task-handler)
- slug: data-analyzer
name: 📊 Data Analyzer
roleDefinition: Specialized mode for analyzing data structures and patterns
description: Analyzes data and provides insights
groups:
- read
hidden: true
parent: complex-task-handler
customInstructions: |-
Focus exclusively on data analysis:
- Examine data structures
- Identify patterns
- Generate insights
- Return findings to parent mode

# Hidden submode for code generation (only accessible from complex-task-handler)
- slug: code-generator
name: ⚙️ Code Generator
roleDefinition: Specialized mode for generating optimized code
description: Generates code based on specifications
groups:
- read
- edit
hidden: true
parent: complex-task-handler
customInstructions: |-
Focus exclusively on code generation:
- Generate clean, efficient code
- Follow best practices
- Add appropriate comments
- Return to parent mode when complete

# Hidden submode for test writing (only accessible from complex-task-handler)
- slug: test-writer
name: 🧪 Test Writer
roleDefinition: Specialized mode for writing comprehensive tests
description: Writes unit and integration tests
groups:
- read
- edit
hidden: true
parent: complex-task-handler
customInstructions: |-
Focus exclusively on test creation:
- Write comprehensive test cases
- Cover edge cases
- Ensure good test coverage
- Return to parent mode when complete

# Regular visible mode (shown in dropdown)
- slug: documentation-mode
name: 📚 Documentation
roleDefinition: Mode for creating and updating documentation
description: Creates and maintains documentation
groups:
- read
- edit
customInstructions: |-
Focus on documentation tasks:
- Write clear, comprehensive docs
- Update existing documentation
- Create examples and tutorials
2 changes: 2 additions & 0 deletions packages/types/src/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const modeConfigSchema = z.object({
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
hidden: z.boolean().optional(),
parent: z.string().optional(),
Comment on lines +73 to +74
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The schema allows hidden: true without requiring parent, which would create modes that are completely inaccessible. If a mode is hidden but has no parent, there's no way to switch to it. The schema should enforce that if hidden is true, then parent must be provided.

Consider adding a .refine() validation like:

.refine(
  (data) => !data.hidden || data.parent,
  { message: "Hidden modes must specify a parent mode" }
)

Fix it with Roo Code or mention @roomote and request a fix.

})

export type ModeConfig = z.infer<typeof modeConfigSchema>
Expand Down
26 changes: 22 additions & 4 deletions src/core/tools/SwitchModeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> {

task.consecutiveMistakeCount = 0

// Verify the mode exists
const targetMode = getModeBySlug(mode_slug, (await task.providerRef.deref()?.getState())?.customModes)
const state = await task.providerRef.deref()?.getState()
const customModes = state?.customModes

// Verify the mode exists (including hidden modes)
const targetMode = getModeBySlug(mode_slug, customModes)

if (!targetMode) {
task.recordToolError("switch_mode")
Expand All @@ -45,7 +48,22 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> {
}

// Check if already in requested mode
const currentMode = (await task.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
const currentMode = state?.mode ?? defaultModeSlug
const currentModeConfig = getModeBySlug(currentMode, customModes)

// Check if the target mode is hidden
if (targetMode.hidden) {
// Hidden modes can only be accessed by their parent mode
if (!targetMode.parent || targetMode.parent !== currentMode) {
task.recordToolError("switch_mode")
pushToolResult(
formatResponse.toolError(
`Mode '${mode_slug}' is not accessible from the current mode '${currentMode}'.`,
),
)
return
}
}
Comment on lines +54 to +66
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code checks if targetMode.parent equals currentMode, but it doesn't verify that the parent mode actually exists in the available modes. If someone defines a hidden mode with a non-existent parent slug, this validation would pass (assuming you're in that non-existent mode somehow), but the parent-child relationship would be invalid.

Consider adding validation to ensure the parent mode exists:

if (targetMode.hidden) {
    if (!targetMode.parent) {
        // error: hidden mode must have parent
    }
    const parentMode = getModeBySlug(targetMode.parent, customModes)
    if (!parentMode) {
        // error: parent mode does not exist
    }
    if (targetMode.parent !== currentMode) {
        // error: can only be accessed by parent
    }
}

Fix it with Roo Code or mention @roomote and request a fix.


if (currentMode === mode_slug) {
task.recordToolError("switch_mode")
Expand All @@ -64,7 +82,7 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> {
await task.providerRef.deref()?.handleModeSwitch(mode_slug)

pushToolResult(
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
`Successfully switched from ${currentModeConfig?.name ?? currentMode} mode to ${
targetMode.name
} mode${reason ? ` because: ${reason}` : ""}.`,
)
Expand Down
118 changes: 118 additions & 0 deletions src/shared/__tests__/modes.hidden.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect } from "vitest"
import { type ModeConfig } from "@roo-code/types"
import { getAllModes, getModeBySlug } from "../modes"

describe("Hidden Modes", () => {
const customModes: ModeConfig[] = [
{
slug: "parent-mode",
name: "Parent Mode",
roleDefinition: "Parent mode role",
groups: ["read", "edit"],
},
{
slug: "hidden-submode",
name: "Hidden Submode",
roleDefinition: "Hidden submode role",
groups: ["read"],
hidden: true,
parent: "parent-mode",
},
{
slug: "visible-mode",
name: "Visible Mode",
roleDefinition: "Visible mode role",
groups: ["read"],
},
]

describe("getAllModes", () => {
it("should exclude hidden modes by default", () => {
const modes = getAllModes(customModes)
const slugs = modes.map((m) => m.slug)

expect(slugs).toContain("parent-mode")
expect(slugs).toContain("visible-mode")
expect(slugs).not.toContain("hidden-submode")
})

it("should include hidden modes when includeHidden is true", () => {
const modes = getAllModes(customModes, true)
const slugs = modes.map((m) => m.slug)

expect(slugs).toContain("parent-mode")
expect(slugs).toContain("visible-mode")
expect(slugs).toContain("hidden-submode")
})

it("should return all built-in modes when no custom modes provided", () => {
const modes = getAllModes()
expect(modes.length).toBeGreaterThan(0)
expect(modes.every((m) => !m.hidden)).toBe(true)
})

it("should override built-in modes with custom modes of same slug", () => {
const customWithOverride: ModeConfig[] = [
{
slug: "code",
name: "Custom Code Mode",
roleDefinition: "Custom code role",
groups: ["read"],
},
]

const modes = getAllModes(customWithOverride)
const codeMode = modes.find((m) => m.slug === "code")

expect(codeMode?.name).toBe("Custom Code Mode")
})
})

describe("getModeBySlug", () => {
it("should find hidden modes", () => {
const mode = getModeBySlug("hidden-submode", customModes)
expect(mode).toBeDefined()
expect(mode?.hidden).toBe(true)
expect(mode?.parent).toBe("parent-mode")
})

it("should find visible modes", () => {
const mode = getModeBySlug("parent-mode", customModes)
expect(mode).toBeDefined()
expect(mode?.hidden).toBeUndefined()
})

it("should return undefined for non-existent mode", () => {
const mode = getModeBySlug("non-existent", customModes)
expect(mode).toBeUndefined()
})
})

describe("Hidden mode parent-child relationships", () => {
it("should correctly identify parent-child relationships", () => {
const hiddenMode = getModeBySlug("hidden-submode", customModes)
const parentMode = getModeBySlug("parent-mode", customModes)

expect(hiddenMode?.parent).toBe(parentMode?.slug)
})

it("should allow multiple hidden modes with same parent", () => {
const modesWithMultipleChildren: ModeConfig[] = [
...customModes,
{
slug: "hidden-submode-2",
name: "Hidden Submode 2",
roleDefinition: "Hidden submode 2 role",
groups: ["read"],
hidden: true,
parent: "parent-mode",
},
]

const modes = getAllModes(modesWithMultipleChildren, true)
const hiddenModes = modes.filter((m) => m.hidden && m.parent === "parent-mode")

expect(hiddenModes.length).toBe(2)
})
})
})
14 changes: 11 additions & 3 deletions src/shared/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeCon
}

// Get all available modes, with custom modes overriding built-in modes
export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
export function getAllModes(customModes?: ModeConfig[], includeHidden: boolean = false): ModeConfig[] {
if (!customModes?.length) {
return [...modes]
}
Expand All @@ -106,6 +106,11 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
}
})

// Filter out hidden modes unless explicitly requested
if (!includeHidden) {
return allModes.filter((mode) => !mode.hidden)
}

return allModes
}

Expand Down Expand Up @@ -284,11 +289,14 @@ export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
)

// Helper function to get all modes with their prompt overrides from extension state
export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise<ModeConfig[]> {
export async function getAllModesWithPrompts(
context: vscode.ExtensionContext,
includeHidden: boolean = false,
): Promise<ModeConfig[]> {
const customModes = (await context.globalState.get<ModeConfig[]>("customModes")) || []
const customModePrompts = (await context.globalState.get<CustomModePrompts>("customModePrompts")) || {}

const allModes = getAllModes(customModes)
const allModes = getAllModes(customModes, includeHidden)
return allModes.map((mode) => ({
...mode,
roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition,
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [inputValue, setInputValue, t])

const allModes = useMemo(() => getAllModes(customModes), [customModes])
const allModes = useMemo(() => getAllModes(customModes, false), [customModes])

// Memoized check for whether the input has content (text or images)
const hasInputContent = useMemo(() => {
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro

// Function to handle mode switching
const switchToNextMode = useCallback(() => {
const allModes = getAllModes(customModes)
const allModes = getAllModes(customModes, false)
const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
const nextModeIndex = (currentModeIndex + 1) % allModes.length
// Update local state and notify extension to sync mode change
Expand All @@ -1298,7 +1298,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro

// Function to handle switching to previous mode
const switchToPreviousMode = useCallback(() => {
const allModes = getAllModes(customModes)
const allModes = getAllModes(customModes, false)
const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length
// Update local state and notify extension to sync mode change
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/components/chat/ModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export const ModeSelector = ({

// Get all modes including custom modes and merge custom prompt descriptions.
const modes = React.useMemo(() => {
const allModes = getAllModes(customModes)
// Don't include hidden modes in the selector dropdown
const allModes = getAllModes(customModes, false)

return allModes.map((mode) => ({
...mode,
Expand Down
7 changes: 4 additions & 3 deletions webview-ui/src/components/modes/ModesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
const [visualMode, setVisualMode] = useState(mode)

// Build modes fresh each render so search reflects inline rename updates immediately
const modes = getAllModes(customModes)
// Include hidden modes in the settings view so they can be managed
const modes = getAllModes(customModes, true)

const [isDialogOpen, setIsDialogOpen] = useState(false)
const [selectedPromptContent, setSelectedPromptContent] = useState("")
Expand Down Expand Up @@ -537,8 +538,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
if (message.success) {
const { slug } = message as ImportModeResult
if (slug) {
// Try switching using the freshest mode list available
const all = getAllModes(customModesRef.current)
// Try switching using the freshest mode list available (include hidden modes)
const all = getAllModes(customModesRef.current, true)
const importedMode = all.find((m) => m.slug === slug)
if (importedMode) {
handleModeSwitchRef.current(importedMode)
Expand Down
Loading